From 9cc92b803550ba5954a67c10882df79b7205a42c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Feb 2022 16:24:47 +0100 Subject: [PATCH 01/12] 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 b6348803a4..3c0bf9edf3 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -94,7 +94,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) /// /// 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 0000000000..643c1959ae --- /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 0000000000..f6beebf757 --- /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 0000000000..ffb1ddc1f6 --- /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 0000000000..15d2d01452 --- /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 0000000000..b93b4d66a4 --- /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 9d18e5d821..447f7f781d 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 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken { 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken } // 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 @@ private uint ReadImageHeader() // 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 @@ private WebpImageInfo ReadVp8Info() 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f 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 b26e4101e0..b0131e07ab 100644 --- a/src/ImageSharp/Formats/Webp/WebpFeatures.cs +++ b/src/ImageSharp/Formats/Webp/WebpFeatures.cs @@ -46,6 +46,17 @@ internal class WebpFeatures : IDisposable /// 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 1904b09790..cd18282496 100644 --- a/src/ImageSharp/Primitives/Rectangle.cs +++ b/src/ImageSharp/Primitives/Rectangle.cs @@ -81,7 +81,7 @@ public Rectangle(Point point, Size size) 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 @@ public Point Location 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 @@ public int Bottom /// /// 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 @@ public int Bottom [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 @@ public int Bottom /// 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 @@ public void Inflate(int width, int height) 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 @@ public bool Contains(Rectangle rectangle) => (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 @@ public void Offset(int dx, int dy) } /// - 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 7a24469597..ccb3e3fb97 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -21,7 +21,7 @@ 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; From 00e91cb6ab7c4e9f2acf11674af26a6a2ea8b468 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Feb 2022 11:42:01 +0100 Subject: [PATCH 02/12] Add alpha blending for animated webp --- .../Formats/Webp/WebpAnimationDecoder.cs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 15d2d01452..b3cf4b00fd 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -146,23 +146,23 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag imageFrame = currentFrame; } + int frameX = (int)(frameData.X * 2); + int frameY = (int)(frameData.Y * 2); + int frameWidth = (int)frameData.Width; + int frameHeight = (int)frameData.Height; + var regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); + 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); + this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); } previousFrame = currentFrame ?? image.Frames.RootFrame; @@ -207,17 +207,17 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp /// 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) + private void DrawDecodedImageOnCanvas(Image decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int 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++) + for (int y = frameY; y < frameY + frameHeight; y++) { - Span framePixelRow = imageFramePixels.DangerousGetRowSpan((int)y); - Span decodedPixelRow = decodedImagePixels.DangerousGetRowSpan(decodedRowIdx++).Slice(0, (int)frameWidth); - decodedPixelRow.TryCopyTo(framePixelRow.Slice((int)frameX)); + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImagePixels.DangerousGetRowSpan(decodedRowIdx++).Slice(0, frameWidth); + decodedPixelRow.TryCopyTo(framePixelRow.Slice(frameX)); } } @@ -228,30 +228,37 @@ private void DrawDecodedImageOnCanvas(Image decodedImage, ImageF /// The pixel format. /// The source image. /// The destination image. - private void AlphaBlend(ImageFrame src, ImageFrame dst) + /// The frame x coordinate. + /// The frame y coordinate. + /// The width of the frame. + /// The height of the frame. + private void AlphaBlend(ImageFrame src, ImageFrame dst, int frameX, int frameY, int frameWidth, int frameHeight) 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++) + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + for (int y = frameY; y < frameY + frameHeight; y++) { Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - for (int x = 0; x < width; x++) + for (int x = frameX; x < frameX + frameWidth; 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) + + if (srcRgba.A is 0) + { + dstPixel.FromRgba32(dstRgba); + } + else { - Rgba32 blendResult = blender.Blend(srcRgba, dstRgba, 1.0f); + int dstFactorA = dstRgba.A * (255 - srcRgba.A) / 255; + Rgba32 blendResult = blender.Blend(srcRgba, dstRgba, 1.0f / dstFactorA); dstPixel.FromRgba32(blendResult); } } From 2b8af22d5d2defb21432d58b9d2e10e4011dbe87 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Feb 2022 15:28:30 +0100 Subject: [PATCH 03/12] Add decoding animated lossy webp --- .../Webp/Lossless/WebpLosslessDecoder.cs | 2 +- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 13 +++++- .../Formats/Webp/WebpAnimationDecoder.cs | 42 ++++++++++++++++--- .../Formats/Webp/WebpChunkParsingUtils.cs | 2 +- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 4 +- .../Formats/Webp/WebpDecoderCore.cs | 36 ++++++++++++---- src/ImageSharp/Formats/Webp/WebpFeatures.cs | 13 +----- src/ImageSharp/Formats/Webp/WebpImageInfo.cs | 1 - 8 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index f517ad520f..2d2396f1a5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -87,7 +87,7 @@ public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAlloca private static ReadOnlySpan LiteralMap => new byte[] { 0, 1, 1, 1, 0 }; /// - /// Decodes the image from the stream using the bitreader. + /// Decodes the lossless webp image from the stream. /// /// The pixel format. /// The pixel buffer to store the decoded data. diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index b74f6969e1..d374393e9a 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -57,7 +57,16 @@ public WebpLossyDecoder(Vp8BitReader bitReader, MemoryAllocator memoryAllocator, this.configuration = configuration; } - public void Decode(Buffer2D pixels, int width, int height, WebpImageInfo info) + /// + /// Decodes the lossless webp image from the stream. + /// + /// The pixel format. + /// The pixel buffer to store the decoded data. + /// The width of the image. + /// The height of the image. + /// Information about the image. + /// The ALPH chunk data. + public void Decode(Buffer2D pixels, int width, int height, WebpImageInfo info, IMemoryOwner alphaData) where TPixel : unmanaged, IPixel { // Paragraph 9.2: color space and clamp type follow. @@ -105,7 +114,7 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI using (var alphaDecoder = new AlphaDecoder( width, height, - info.Features.AlphaData, + alphaData, info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index b3cf4b00fd..90a5142eb7 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// /// Decoder for animated webp images. /// - internal class WebpAnimationDecoder + internal class WebpAnimationDecoder : IDisposable { /// /// Reusable buffer. @@ -47,6 +48,11 @@ public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration confi this.configuration = configuration; } + /// + /// Gets or sets the alpha data, if an ALPH chunk is present. + /// + public IMemoryOwner AlphaData { get; set; } + /// /// Decodes the animated webp image from the specified stream. /// @@ -108,12 +114,12 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag long streamStartPosition = stream.Position; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + bool hasAlpha = false; + byte alphaChunkHeader = 0; if (chunkType is WebpChunkType.Alpha) { - // TODO: ignore alpha for now. - stream.Skip(4); - uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); - stream.Skip((int)alphaChunkSize); + alphaChunkHeader = this.ReadAlphaData(stream); + hasAlpha = true; chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); } @@ -123,6 +129,8 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag { case WebpChunkType.Vp8: webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); + features.Alpha = hasAlpha; + features.AlphaChunkHeader = alphaChunkHeader; break; case WebpChunkType.Vp8L: webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); @@ -171,6 +179,25 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag return (uint)(stream.Position - streamStartPosition); } + /// + /// Reads the ALPH chunk data. + /// + /// The stream to read from. + private byte ReadAlphaData(BufferedReadStream stream) + { + this.AlphaData?.Dispose(); + + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); + int alphaDataSize = (int)(alphaChunkSize - 1); + this.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); + + byte alphaChunkHeader = (byte)stream.ReadByte(); + Span alphaData = this.AlphaData.GetSpan(); + stream.Read(alphaData, 0, alphaDataSize); + + return alphaChunkHeader; + } + /// /// Decodes the either lossy or lossless webp image data. /// @@ -191,7 +218,7 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp else { var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); - lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo); + lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.AlphaData); } return decodedImage; @@ -326,5 +353,8 @@ private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) return data; } + + /// + public void Dispose() => this.AlphaData?.Dispose(); } } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index b93b4d66a4..5b8e1857c7 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -25,7 +25,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Strea stream.Read(buffer, 0, 4); uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - // remaining counts the available image data payload. + // Remaining counts the available image data payload. uint remaining = dataSize; // Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30 diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index b4e6cecd0c..1736e97ce2 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -26,7 +26,7 @@ public Image Decode(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - var decoder = new WebpDecoderCore(configuration, this); + using var decoder = new WebpDecoderCore(configuration, this); try { @@ -57,7 +57,7 @@ public Task> DecodeAsync(Configuration configuration, Stre { Guard.NotNull(stream, nameof(stream)); - var decoder = new WebpDecoderCore(configuration, this); + using var decoder = new WebpDecoderCore(configuration, this); try { diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 447f7f781d..cde8f612bb 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers; using System.Buffers.Binary; using System.IO; using System.Threading; @@ -19,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// /// Performs the webp decoding operation. /// - internal sealed class WebpDecoderCore : IImageDecoderInternals + internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { /// /// Reusable buffer. @@ -76,6 +78,11 @@ public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options) /// public Size Dimensions => new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height); + /// + /// Gets or sets the alpha data, if an ALPH chunk is present. + /// + public IMemoryOwner AlphaData { get; set; } + /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -89,7 +96,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (this.webImageInfo.Features is { Animation: true }) { - var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration); + using var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -104,7 +111,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken else { var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration); - lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo); + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.AlphaData); } // There can be optional chunks after the image data, like EXIF and XMP. @@ -120,7 +127,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella this.currentStream = stream; this.ReadImageHeader(); - using (this.webImageInfo = this.ReadVp8Info()) + using (this.webImageInfo = this.ReadVp8Info(true)) { return new ImageInfo(new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); } @@ -149,8 +156,9 @@ private uint ReadImageHeader() /// /// Reads information present in the image header, about the image content and how to decode the image. /// + /// For identify, the alpha data should not be read. /// Information about the webp image. - private WebpImageInfo ReadVp8Info() + private WebpImageInfo ReadVp8Info(bool ignoreAlpha = false) { this.Metadata = new ImageMetadata(); this.webpMetadata = this.Metadata.GetFormatMetadata(WebpFormat.Instance); @@ -183,7 +191,7 @@ private WebpImageInfo ReadVp8Info() } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { - bool isAnimationChunk = this.ParseOptionalExtendedChunks(chunkType, features); + bool isAnimationChunk = this.ParseOptionalExtendedChunks(chunkType, features, ignoreAlpha); if (isAnimationChunk) { return webpInfos; @@ -207,8 +215,9 @@ private WebpImageInfo ReadVp8Info() /// /// The chunk type. /// The webp image features. + /// For identify, the alpha data should not be read. /// true, if animation chunk was found. - private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features) + private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features, bool ignoreAlpha) { int bytesRead; switch (chunkType) @@ -293,10 +302,16 @@ private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f case WebpChunkType.Alpha: uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer); + if (ignoreAlpha) + { + this.currentStream.Skip((int)alphaChunkSize); + } + features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); int alphaDataSize = (int)(alphaChunkSize - 1); - features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); - this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + this.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); + Span alphaData = this.AlphaData.GetSpan(); + this.currentStream.Read(alphaData, 0, alphaDataSize); break; default: WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); @@ -324,5 +339,8 @@ private void ReadOptionalMetadata() WebpChunkParsingUtils.ParseOptionalChunks(this.currentStream, chunkType, this.Metadata, this.IgnoreMetadata, this.buffer); } } + + /// + public void Dispose() => this.AlphaData?.Dispose(); } } diff --git a/src/ImageSharp/Formats/Webp/WebpFeatures.cs b/src/ImageSharp/Formats/Webp/WebpFeatures.cs index b0131e07ab..398514d5bd 100644 --- a/src/ImageSharp/Formats/Webp/WebpFeatures.cs +++ b/src/ImageSharp/Formats/Webp/WebpFeatures.cs @@ -1,15 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Buffers; - namespace SixLabors.ImageSharp.Formats.Webp { /// /// Image features of a VP8X image. /// - internal class WebpFeatures : IDisposable + internal class WebpFeatures { /// /// Gets or sets a value indicating whether this image has an ICC Profile. @@ -21,11 +18,6 @@ internal class WebpFeatures : IDisposable /// public bool Alpha { get; set; } - /// - /// Gets or sets the alpha data, if an ALPH chunk is present. - /// - public IMemoryOwner AlphaData { get; set; } - /// /// Gets or sets the alpha chunk header. /// @@ -56,8 +48,5 @@ internal class WebpFeatures : IDisposable /// 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/Formats/Webp/WebpImageInfo.cs b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs index 530f5c0a5a..aa11d38c38 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs @@ -63,7 +63,6 @@ public void Dispose() { this.Vp8BitReader?.Dispose(); this.Vp8LBitReader?.Dispose(); - this.Features?.AlphaData?.Dispose(); } } } From 7d0dd42b0d7a2b0512164378528029e052892804 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Feb 2022 17:54:15 +0100 Subject: [PATCH 04/12] Replace broken webp image with exif data --- .../Formats/WebP/WebpDecoderTests.cs | 4 +-- .../Formats/WebP/WebpEncoderTests.cs | 2 +- .../Formats/WebP/WebpMetaDataTests.cs | 28 ++++++++++++++++--- tests/ImageSharp.Tests/TestImages.cs | 5 ++-- ...sless_small.webp => bike_lossy_small.webp} | 0 ...e_lossy.webp => bike_lossy_with_exif.webp} | 0 6 files changed, 29 insertions(+), 10 deletions(-) rename tests/Images/Input/Webp/{bike_lossless_small.webp => bike_lossy_small.webp} (100%) rename tests/Images/Input/Webp/{bike_lossy.webp => bike_lossy_with_exif.webp} (100%) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 1c92fdf335..747e9db935 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -33,7 +33,7 @@ public class WebpDecoderTests [InlineData(Lossless.NoTransform2, 128, 128, 32)] [InlineData(Lossy.Alpha1, 1000, 307, 32)] [InlineData(Lossy.Alpha2, 1000, 307, 32)] - [InlineData(Lossy.Bike, 250, 195, 24)] + [InlineData(Lossy.BikeWithExif, 250, 195, 24)] public void Identify_DetectsCorrectDimensionsAndBitDepth( string imagePath, int expectedWidth, @@ -52,7 +52,7 @@ public void Identify_DetectsCorrectDimensionsAndBitDepth( } [Theory] - [WithFile(Lossy.Bike, PixelTypes.Rgba32)] + [WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32)] [WithFile(Lossy.NoFilter01, PixelTypes.Rgba32)] [WithFile(Lossy.NoFilter02, PixelTypes.Rgba32)] [WithFile(Lossy.NoFilter03, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 7c74429edc..21056bdc61 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -20,7 +20,7 @@ public class WebpEncoderTests [Theory] [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] - [WithFile(Lossy.Bike, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] + [WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] public void Encode_PreserveRatio(TestImageProvider provider, WebpFileFormatType expectedFormat) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 7fba86b4fe..99bc152283 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -16,11 +16,31 @@ public class WebpMetaDataTests private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; [Theory] - [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, false)] - [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, true)] + [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)] + public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLossyImage(TestImageProvider provider, bool ignoreMetadata) + where TPixel : unmanaged, IPixel + { + var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata }; + + using Image image = provider.GetImage(decoder); + if (ignoreMetadata) + { + Assert.Null(image.Metadata.ExifProfile); + } + else + { + ExifProfile exifProfile = image.Metadata.ExifProfile; + Assert.NotNull(exifProfile); + Assert.NotEmpty(exifProfile.Values); + Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2")); + } + } + + [Theory] [WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, true)] - public void IgnoreMetadata_ControlsWhetherExifIsParsed(TestImageProvider provider, bool ignoreMetadata) + public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLosslessImage(TestImageProvider provider, bool ignoreMetadata) where TPixel : unmanaged, IPixel { var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata }; @@ -109,7 +129,7 @@ public void Encode_WritesExifWithPadding(WebpFileFormatType fileFormatType) } [Theory] - [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)] + [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32)] public void EncodeLossyWebp_PreservesExif(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8b943194a5..9ed9a48797 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -632,13 +632,12 @@ public static class Lossless public static class Lossy { public const string Earth = "Webp/earth_lossy.webp"; - public const string WithExif = "Webp/exif_lossy.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp"; public const string WithXmp = "Webp/xmp_lossy.webp"; - public const string BikeSmall = "Webp/bike_lossless_small.webp"; + public const string BikeSmall = "Webp/bike_lossy_small.webp"; // Lossy images without macroblock filtering. - public const string Bike = "Webp/bike_lossy.webp"; + public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; public const string NoFilter01 = "Webp/vp80-01-intra-1400.webp"; public const string NoFilter02 = "Webp/vp80-00-comprehensive-010.webp"; public const string NoFilter03 = "Webp/vp80-00-comprehensive-005.webp"; diff --git a/tests/Images/Input/Webp/bike_lossless_small.webp b/tests/Images/Input/Webp/bike_lossy_small.webp similarity index 100% rename from tests/Images/Input/Webp/bike_lossless_small.webp rename to tests/Images/Input/Webp/bike_lossy_small.webp diff --git a/tests/Images/Input/Webp/bike_lossy.webp b/tests/Images/Input/Webp/bike_lossy_with_exif.webp similarity index 100% rename from tests/Images/Input/Webp/bike_lossy.webp rename to tests/Images/Input/Webp/bike_lossy_with_exif.webp From c93007c58bb61115ed26efc340281409f5386722 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 9 Feb 2022 19:20:21 +0100 Subject: [PATCH 05/12] Fix identify webp with alpha --- src/ImageSharp/Formats/Webp/WebpDecoderCore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index cde8f612bb..01e604a4be 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -305,6 +305,7 @@ private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f if (ignoreAlpha) { this.currentStream.Skip((int)alphaChunkSize); + break; } features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); From 54a43908b3526cb0738686a83658ee1da9b48536 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 10 Feb 2022 12:54:57 +0100 Subject: [PATCH 06/12] Add tests for animated webp --- .../Formats/Webp/WebpAnimationDecoder.cs | 6 +---- .../Formats/Gif/GifDecoderTests.cs | 2 +- .../Formats/WebP/WebpDecoderTests.cs | 25 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 10 ++------ .../00.png | 3 +++ .../01.png | 3 +++ .../02.png | 3 +++ .../03.png | 3 +++ .../04.png | 3 +++ .../05.png | 3 +++ .../06.png | 3 +++ .../07.png | 3 +++ .../08.png | 3 +++ .../09.png | 3 +++ .../10.png | 3 +++ .../11.png | 3 +++ .../00.png | 3 +++ .../01.png | 3 +++ .../02.png | 3 +++ .../03.png | 3 +++ .../04.png | 3 +++ .../05.png | 3 +++ .../06.png | 3 +++ .../07.png | 3 +++ .../08.png | 3 +++ .../09.png | 3 +++ .../10.png | 3 +++ .../11.png | 3 +++ .../Input/Webp/leo_animated_lossless.webp | 3 +++ .../Images/Input/Webp/leo_animated_lossy.webp | 3 +++ 30 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png create mode 100644 tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png create mode 100644 tests/Images/Input/Webp/leo_animated_lossless.webp create mode 100644 tests/Images/Input/Webp/leo_animated_lossy.webp diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 90a5142eb7..b31cf7a264 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -278,11 +278,7 @@ private void AlphaBlend(ImageFrame src, ImageFrame dst, srcPixel.ToRgba32(ref srcRgba); dstPixel.ToRgba32(ref dstRgba); - if (srcRgba.A is 0) - { - dstPixel.FromRgba32(dstRgba); - } - else + if (srcRgba.A is not 0) { int dstFactorA = dstRgba.A * (255 - srcRgba.A) / 255; Rgba32 blendResult = blender.Blend(srcRgba, dstRgba, 1.0f / dstFactorA); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index c8ecdb717b..250a0e9920 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -130,7 +130,7 @@ public void DetectPixelSize(string imagePath, int expectedPixelSize) public void Decode_WithInvalidDimensions_DoesThrowException(TestImageProvider provider) where TPixel : unmanaged, IPixel { - System.Exception ex = Record.Exception( + Exception ex = Record.Exception( () => { using Image image = provider.GetImage(GifDecoder); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 747e9db935..63eab77bfe 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Webp; @@ -329,6 +330,30 @@ public void WebpDecoder_CanDecode_Lossless_WithThreeTransforms(TestImage } } + [Theory] + [WithFile(Lossless.Animated, PixelTypes.Rgba32)] + public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + } + + [Theory] + [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f)); + } + } + [Theory] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9ed9a48797..eeaae253a0 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -550,16 +550,9 @@ public static class Webp // Test images for converting rgb data to yuv. public const string Yuv = "Webp/yuv_test.png"; - public static class Animated - { - public const string Animated1 = "Webp/animated-webp.webp"; - public const string Animated2 = "Webp/animated2.webp"; - public const string Animated3 = "Webp/animated3.webp"; - public const string Animated4 = "Webp/animated_lossy.webp"; - } - public static class Lossless { + public const string Animated = "Webp/leo_animated_lossless.webp"; public const string Earth = "Webp/earth_lossless.webp"; public const string Alpha = "Webp/lossless_alpha_small.webp"; public const string WithExif = "Webp/exif_lossless.webp"; @@ -635,6 +628,7 @@ public static class Lossy public const string WithIccp = "Webp/lossy_with_iccp.webp"; public const string WithXmp = "Webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossy_small.webp"; + public const string Animated = "Webp/leo_animated_lossy.webp"; // Lossy images without macroblock filtering. public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png new file mode 100644 index 0000000000..ba7d1f98eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5 +size 50298 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png new file mode 100644 index 0000000000..7e5669acb7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad +size 49389 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png new file mode 100644 index 0000000000..38e594a18d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85 +size 52686 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png new file mode 100644 index 0000000000..8fa981590c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559 +size 53244 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png new file mode 100644 index 0000000000..382f196e20 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473 +size 56046 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png new file mode 100644 index 0000000000..79a5f44ec6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de +size 62469 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png new file mode 100644 index 0000000000..3299889d80 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0 +size 61068 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png new file mode 100644 index 0000000000..bc43fac5d4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5 +size 60411 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png new file mode 100644 index 0000000000..7a71a5ff0b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527 +size 58793 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png new file mode 100644 index 0000000000..e8c9eb3f24 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391 +size 57157 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png new file mode 100644 index 0000000000..05d5ab1d0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d +size 55424 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png new file mode 100644 index 0000000000..ae6ce177b0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de +size 59950 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png new file mode 100644 index 0000000000..ba7d1f98eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5 +size 50298 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png new file mode 100644 index 0000000000..7e5669acb7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad +size 49389 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png new file mode 100644 index 0000000000..38e594a18d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85 +size 52686 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png new file mode 100644 index 0000000000..8fa981590c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559 +size 53244 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png new file mode 100644 index 0000000000..382f196e20 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473 +size 56046 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png new file mode 100644 index 0000000000..79a5f44ec6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de +size 62469 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png new file mode 100644 index 0000000000..3299889d80 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0 +size 61068 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png new file mode 100644 index 0000000000..bc43fac5d4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5 +size 60411 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png new file mode 100644 index 0000000000..7a71a5ff0b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527 +size 58793 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png new file mode 100644 index 0000000000..e8c9eb3f24 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391 +size 57157 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png new file mode 100644 index 0000000000..05d5ab1d0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d +size 55424 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png new file mode 100644 index 0000000000..ae6ce177b0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de +size 59950 diff --git a/tests/Images/Input/Webp/leo_animated_lossless.webp b/tests/Images/Input/Webp/leo_animated_lossless.webp new file mode 100644 index 0000000000..3778e4a259 --- /dev/null +++ b/tests/Images/Input/Webp/leo_animated_lossless.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bab815db08e8f413c7a355b7e9c152e1a73e503392012af16ada92858706d255 +size 400342 diff --git a/tests/Images/Input/Webp/leo_animated_lossy.webp b/tests/Images/Input/Webp/leo_animated_lossy.webp new file mode 100644 index 0000000000..3bd434bc27 --- /dev/null +++ b/tests/Images/Input/Webp/leo_animated_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00fffbb0d67b0336574d9bad9cbacaf97d81f2e70db3d458508c430e3d103228 +size 64972 From 4c30ccdf04ea59214dce64188403bcd2e6f14915 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 10 Feb 2022 14:46:10 +0100 Subject: [PATCH 07/12] Add loopcount and duration to the webp metadata --- src/ImageSharp/Formats/Gif/GifFormat.cs | 8 ++--- .../Formats/Webp/MetadataExtensions.cs | 7 ++++ .../Formats/Webp/WebpAnimationDecoder.cs | 36 +++++++++++++++++-- src/ImageSharp/Formats/Webp/WebpFormat.cs | 11 +++--- .../Formats/Webp/WebpFrameMetadata.cs | 33 +++++++++++++++++ src/ImageSharp/Formats/Webp/WebpMetadata.cs | 11 +++++- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 4 +-- src/ImageSharp/Metadata/ImageMetadata.cs | 4 +-- .../Formats/WebP/WebpDecoderTests.cs | 12 +++++++ 9 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs diff --git a/src/ImageSharp/Formats/Gif/GifFormat.cs b/src/ImageSharp/Formats/Gif/GifFormat.cs index 459f0068be..fcb0fe5b3f 100644 --- a/src/ImageSharp/Formats/Gif/GifFormat.cs +++ b/src/ImageSharp/Formats/Gif/GifFormat.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; @@ -17,7 +17,7 @@ private GifFormat() /// /// Gets the current instance. /// - public static GifFormat Instance { get; } = new GifFormat(); + public static GifFormat Instance { get; } = new(); /// public string Name => "GIF"; @@ -32,9 +32,9 @@ private GifFormat() public IEnumerable FileExtensions => GifConstants.FileExtensions; /// - public GifMetadata CreateDefaultFormatMetadata() => new GifMetadata(); + public GifMetadata CreateDefaultFormatMetadata() => new(); /// - public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new GifFrameMetadata(); + public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } } diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs index 63f8e3427e..3a85b5441f 100644 --- a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs @@ -17,5 +17,12 @@ public static partial class MetadataExtensions /// The metadata this method extends. /// The . public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance); + + /// + /// Gets the webp format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The . + public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance); } } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index b31cf7a264..60d41c984c 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -37,6 +38,16 @@ internal class WebpAnimationDecoder : IDisposable /// private Rectangle? restoreArea; + /// + /// The abstract metadata. + /// + private ImageMetadata metadata; + + /// + /// The gif specific metadata. + /// + private WebpMetadata webpMetadata; + /// /// Initializes a new instance of the class. /// @@ -68,6 +79,10 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat Image image = null; ImageFrame previousFrame = null; + this.metadata = new ImageMetadata(); + this.webpMetadata = this.metadata.GetWebpMetadata(); + this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount; + int remainingBytes = (int)completeDataSize; while (remainingBytes > 0) { @@ -140,17 +155,22 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag 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); + image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + + this.SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); + imageFrame = image.Frames.RootFrame; } else { currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection. + + this.SetFrameMetadata(currentFrame.Metadata, frameData.Duration); + imageFrame = currentFrame; } @@ -179,6 +199,18 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag return (uint)(stream.Position - streamStartPosition); } + /// + /// Sets the frames metadata. + /// + /// The metadata. + /// The frame duration. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetFrameMetadata(ImageFrameMetadata meta, uint duration) + { + WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); + frameMetadata.FrameDuration = duration; + } + /// /// Reads the ALPH chunk data. /// diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs index 1f27c4d843..bc3fb09c32 100644 --- a/src/ImageSharp/Formats/Webp/WebpFormat.cs +++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs @@ -6,9 +6,9 @@ namespace SixLabors.ImageSharp.Formats.Webp { /// - /// Registers the image encoders, decoders and mime type detectors for the Webp format + /// Registers the image encoders, decoders and mime type detectors for the Webp format. /// - public sealed class WebpFormat : IImageFormat + public sealed class WebpFormat : IImageFormat { private WebpFormat() { @@ -17,7 +17,7 @@ private WebpFormat() /// /// Gets the current instance. /// - public static WebpFormat Instance { get; } = new WebpFormat(); + public static WebpFormat Instance { get; } = new(); /// public string Name => "Webp"; @@ -32,6 +32,9 @@ private WebpFormat() public IEnumerable FileExtensions => WebpConstants.FileExtensions; /// - public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata(); + public WebpMetadata CreateDefaultFormatMetadata() => new(); + + /// + public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs new file mode 100644 index 0000000000..bebfb9d792 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Provides webp specific metadata information for the image frame. + /// + public class WebpFrameMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public WebpFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; + + /// + /// Gets or sets the frame duration. The time to wait before displaying the next frame, + /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined. + /// + public uint FrameDuration { get; set; } + + /// + public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index f398d3d874..5dd0105024 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -19,13 +19,22 @@ public WebpMetadata() /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private WebpMetadata(WebpMetadata other) => this.FileFormat = other.FileFormat; + private WebpMetadata(WebpMetadata other) + { + this.FileFormat = other.FileFormat; + this.AnimationLoopCount = other.AnimationLoopCount; + } /// /// Gets or sets the webp file format used. Either lossless or lossy. /// public WebpFileFormatType? FileFormat { get; set; } + /// + /// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely. + /// + public ushort AnimationLoopCount { get; set; } = 1; + /// public IDeepCloneable DeepClone() => new WebpMetadata(this); } diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 1cad4ebe86..f8ed18e28a 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata /// public sealed class ImageFrameMetadata : IDeepCloneable { - private readonly Dictionary formatMetadata = new Dictionary(); + private readonly Dictionary formatMetadata = new(); /// /// Initializes a new instance of the class. @@ -67,7 +67,7 @@ internal ImageFrameMetadata(ImageFrameMetadata other) public IptcProfile IptcProfile { get; set; } /// - public ImageFrameMetadata DeepClone() => new ImageFrameMetadata(this); + public ImageFrameMetadata DeepClone() => new(this); /// /// Gets the metadata value associated with the specified key. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 89592f776c..cddb76d100 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -33,7 +33,7 @@ public sealed class ImageMetadata : IDeepCloneable /// public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch; - private readonly Dictionary formatMetadata = new Dictionary(); + private readonly Dictionary formatMetadata = new(); private double horizontalResolution; private double verticalResolution; @@ -175,7 +175,7 @@ public TFormatMetadata GetFormatMetadata(IImageFormat - public ImageMetadata DeepClone() => new ImageMetadata(this); + public ImageMetadata DeepClone() => new(this); /// /// Synchronizes the profiles with the current metadata. diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 63eab77bfe..ae1dddba0a 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -337,8 +337,14 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider image = provider.GetImage(WebpDecoder)) { + WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata(); + image.DebugSaveMultiFrame(provider); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + + Assert.Equal(0, webpMetaData.AnimationLoopCount); + Assert.Equal(150U, frameMetaData.FrameDuration); } } @@ -349,8 +355,14 @@ public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider image = provider.GetImage(WebpDecoder)) { + WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata(); + image.DebugSaveMultiFrame(provider); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f)); + + Assert.Equal(0, webpMetaData.AnimationLoopCount); + Assert.Equal(150U, frameMetaData.FrameDuration); } } From ab8a1ba564c96e31a67f1145a7e1b46853d3e2e6 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 10 Feb 2022 16:44:19 +0100 Subject: [PATCH 08/12] Add option to decode just one frame --- .../Formats/Webp/IWebpDecoderOptions.cs | 7 +++++++ .../Formats/Webp/WebpAnimationDecoder.cs | 11 +++++++++-- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 7 +++++++ src/ImageSharp/Formats/Webp/WebpDecoderCore.cs | 14 ++++++++++---- .../Formats/WebP/WebpDecoderTests.cs | 16 +++++++++++++++- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs index 7bd78da3da..cf607ef69f 100644 --- a/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Metadata; + namespace SixLabors.ImageSharp.Formats.Webp { /// @@ -12,5 +14,10 @@ internal interface IWebpDecoderOptions /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// bool IgnoreMetadata { get; } + + /// + /// Gets the decoding mode for multi-frame images. + /// + FrameDecodingMode DecodingMode { get; } } } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 60d41c984c..16589e50f1 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -53,10 +53,12 @@ internal class WebpAnimationDecoder : IDisposable /// /// The memory allocator. /// The global configuration. - public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration) + /// The frame decoding mode. + public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, FrameDecodingMode decodingMode) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; + this.DecodingMode = decodingMode; } /// @@ -64,6 +66,11 @@ public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration confi /// public IMemoryOwner AlphaData { get; set; } + /// + /// Gets the decoding mode for multi-frame images. + /// + public FrameDecodingMode DecodingMode { get; } + /// /// Decodes the animated webp image from the specified stream. /// @@ -103,7 +110,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat break; } - if (stream.Position == stream.Length) + if (stream.Position == stream.Length || this.DecodingMode is FrameDecodingMode.First) { break; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index 1736e97ce2..71b4e4f23d 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp @@ -20,6 +21,12 @@ public sealed class WebpDecoder : IImageDecoder, IWebpDecoderOptions, IImageInfo /// public bool IgnoreMetadata { get; set; } + /// + /// Gets or sets the decoding mode for multi-frame images. + /// Defaults to All. + /// + public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 01e604a4be..252af7867f 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -56,10 +56,19 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options) { this.Configuration = configuration; + this.DecodingMode = options.DecodingMode; this.memoryAllocator = configuration.MemoryAllocator; this.IgnoreMetadata = options.IgnoreMetadata; } + /// + public Configuration Configuration { get; } + + /// + /// Gets the decoding mode for multi-frame images. + /// + public FrameDecodingMode DecodingMode { get; } + /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// @@ -70,9 +79,6 @@ public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options) /// public ImageMetadata Metadata { get; private set; } - /// - public Configuration Configuration { get; } - /// /// Gets the dimensions of the image. /// @@ -96,7 +102,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (this.webImageInfo.Features is { Animation: true }) { - using var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration); + using var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration, this.DecodingMode); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index ae1dddba0a..5e878f7804 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -3,6 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -234,7 +235,7 @@ public void WebpDecoder_CanDecode_Lossless_WithoutTransforms(TestImagePr // TODO: Reference decoder throws here MagickCorruptImageErrorException, webpinfo also indicates an error here, but decoding the image seems to work. // [WithFile(Lossless.GreenTransform5, PixelTypes.Rgba32)] - public void WebpDecoder_CanDecode_Lossless_WithSubstractGreenTransform( + public void WebpDecoder_CanDecode_Lossless_WithSubtractGreenTransform( TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -345,6 +346,7 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new WebpDecoder() { DecodingMode = FrameDecodingMode.First })) + { + Assert.Equal(1, image.Frames.Count); } } From 89d1582da8ead8de9dc21f9990a5a0a16e9229e8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 28 Apr 2022 14:55:50 +0200 Subject: [PATCH 09/12] Add missing dispose of webpInfo --- .../Formats/Webp/WebpAnimationDecoder.cs | 27 ++++++++++++++----- .../MemoryAllocatorValidator.cs | 6 ++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 16589e50f1..fd16b3d268 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -248,16 +248,29 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp where TPixel : unmanaged, IPixel { var decodedImage = new Image((int)frameData.Width, (int)frameData.Height); - Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; - if (webpInfo.IsLossless) + + try + { + 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, this.AlphaData); + } + } + catch { - var losslessDecoder = new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); - losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); + decodedImage?.Dispose(); + throw; } - else + finally { - var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); - lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.AlphaData); + webpInfo.Dispose(); } return decodedImage; diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs index 13664ee9b2..cdd6754cf1 100644 --- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -59,9 +59,9 @@ public class TestMemoryDiagnostics : IDisposable public void Validate(int expectedAllocationCount) { - var count = this.TotalRemainingAllocated; - var pass = expectedAllocationCount == count; - Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}"); + int count = this.TotalRemainingAllocated; + bool pass = expectedAllocationCount == count; + Assert.True(pass, $"Expected {expectedAllocationCount} undisposed buffers but found {count}"); } public void Dispose() From 2a8418f986436c5ad0dd190bf15f6670b2e77e03 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 May 2022 11:26:56 +0200 Subject: [PATCH 10/12] Review changes --- .../Formats/Webp/WebpAnimationDecoder.cs | 13 ++--- .../Formats/Webp/WebpChunkParsingUtils.cs | 58 +++++++++++++------ .../Formats/Webp/WebpDecoderCore.cs | 2 +- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index fd16b3d268..73246778c1 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -192,7 +192,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image imag this.RestoreToBackground(imageFrame, backgroundColor); } - using Image decodedImage = this.DecodeImageData(frameData, webpInfo); + using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); this.DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) @@ -244,7 +244,7 @@ private byte ReadAlphaData(BufferedReadStream stream) /// The frame data. /// The webp information. /// A decoded image. - private Image DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { var decodedImage = new Image((int)frameData.Width, (int)frameData.Height); @@ -262,6 +262,8 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.AlphaData); } + + return pixelBufferDecoded; } catch { @@ -272,8 +274,6 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp { webpInfo.Dispose(); } - - return decodedImage; } /// @@ -286,16 +286,15 @@ private Image DecodeImageData(AnimationFrameData frameData, Webp /// The frame y coordinate. /// The width of the frame. /// The height of the frame. - private void DrawDecodedImageOnCanvas(Image decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) + private void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) where TPixel : unmanaged, IPixel { - Buffer2D decodedImagePixels = decodedImage.Frames.RootFrame.PixelBuffer; Buffer2D imageFramePixels = imageFrame.PixelBuffer; int decodedRowIdx = 0; for (int y = frameY; y < frameY + frameHeight; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImagePixels.DangerousGetRowSpan(decodedRowIdx++).Slice(0, frameWidth); + Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++).Slice(0, frameWidth); decodedPixelRow.TryCopyTo(framePixelRow.Slice(frameX)); } } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 5b8e1857c7..26d82a8929 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -3,9 +3,9 @@ using System; using System.Buffers.Binary; -using System.IO; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -19,10 +19,15 @@ 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) + public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) { // VP8 data size (not including this 4 bytes). - stream.Read(buffer, 0, 4); + int bytesRead = stream.Read(buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); + } + uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); // Remaining counts the available image data payload. @@ -34,7 +39,12 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Strea // - 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); + bytesRead = stream.Read(buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); + } + uint frameTag = (uint)(buffer[0] | (buffer[1] << 8) | (buffer[2] << 16)); remaining -= 3; bool isNoKeyFrame = (frameTag & 0x1) == 1; @@ -62,17 +72,27 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Strea } // Check for VP8 magic bytes. - stream.Read(buffer, 0, 3); + bytesRead = stream.Read(buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); + } + 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); + bytesRead = stream.Read(buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header, could not read width and height"); + } + + uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); - tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(buffer.AsSpan(2)); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2)); uint height = tmp & 0x3fff; sbyte yScale = (sbyte)(tmp >> 6); remaining -= 7; @@ -121,7 +141,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Strea /// Reads the header of a lossless webp image. /// /// Information about this image. - public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Stream stream, byte[] buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) { // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); @@ -176,7 +196,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Stre /// 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) + public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buffer, WebpFeatures features) { uint fileSize = ReadChunkSize(stream, buffer); @@ -234,11 +254,15 @@ public static WebpImageInfo ReadVp8XHeader(Stream stream, byte[] buffer, WebpFea /// 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) + public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer) { - stream.Read(buffer, 0, 3); - buffer[3] = 0; - return (uint)BinaryPrimitives.ReadInt32LittleEndian(buffer); + if (stream.Read(buffer, 0, 3) == 3) + { + buffer[3] = 0; + return BinaryPrimitives.ReadUInt32LittleEndian(buffer); + } + + throw new ImageFormatException("Invalid Webp data, could not read unsigned integer."); } /// @@ -248,7 +272,7 @@ public static uint ReadUnsignedInt24Bit(Stream stream, byte[] buffer) /// 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) + public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer) { if (stream.Read(buffer, 0, 4) == 4) { @@ -267,7 +291,7 @@ public static uint ReadChunkSize(Stream stream, byte[] buffer) /// /// Thrown if the input stream is not valid. /// - public static WebpChunkType ReadChunkType(Stream stream, byte[] buffer) + public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buffer) { if (stream.Read(buffer, 0, 4) == 4) { @@ -283,7 +307,7 @@ public static WebpChunkType ReadChunkType(Stream stream, byte[] buffer) /// 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) + public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, byte[] buffer) { long streamLength = stream.Length; while (stream.Position < streamLength) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 84ee5d0a2c..979ac55825 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -36,7 +36,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// /// The stream to decode from. /// - private Stream currentStream; + private BufferedReadStream currentStream; /// /// The webp specific metadata. From 8528f1a33ecffc65887c94c32a48d64ffb7f12eb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 May 2022 14:31:42 +1000 Subject: [PATCH 11/12] Optimize RestoreToBackground --- .../Formats/Webp/WebpAnimationDecoder.cs | 11 ++------- src/ImageSharp/Memory/Buffer2DRegion{T}.cs | 23 +++++++++++++++++++ .../MemoryGroupExtensions.cs | 11 +++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 73246778c1..93ded17f82 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -356,15 +356,8 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba 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); - } - } + TPixel backgroundPixel = backgroundColor.ToPixel(); + pixelRegion.Fill(backgroundPixel); } /// diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index 13b3395977..9b9c1aa5b0 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -141,6 +141,9 @@ internal ref T GetReferenceToOrigin() return ref this.Buffer.DangerousGetRowSpan(y)[x]; } + /// + /// Clears the contents of this . + /// internal void Clear() { // Optimization for when the size of the area is the same as the buffer size. @@ -156,5 +159,25 @@ internal void Clear() row.Clear(); } } + + /// + /// Fills the elements of this with the specified value. + /// + /// The value to assign to each element of the region. + internal void Fill(T value) + { + // Optimization for when the size of the area is the same as the buffer size. + if (this.IsFullBufferArea) + { + this.Buffer.FastMemoryGroup.Fill(value); + return; + } + + for (int y = 0; y < this.Rectangle.Height; y++) + { + Span row = this.DangerousGetRowSpan(y); + row.Fill(value); + } + } } } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs index d200b223a7..7a4e0df5dc 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -7,6 +7,12 @@ namespace SixLabors.ImageSharp.Memory { internal static class MemoryGroupExtensions { + /// + /// Fills the elements of this with the specified value. + /// + /// The type of element. + /// The group to fill. + /// The value to assign to each element of the group. internal static void Fill(this IMemoryGroup group, T value) where T : struct { @@ -16,6 +22,11 @@ internal static void Fill(this IMemoryGroup group, T value) } } + /// + /// Clears the contents of this . + /// + /// The type of element. + /// The group to clear. internal static void Clear(this IMemoryGroup group) where T : struct { From 980a1f043cb88abaca4972ec326058af2aa03c69 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 17 May 2022 14:14:09 +0200 Subject: [PATCH 12/12] Use bulk conversion in AlphaBlend --- .../Formats/Webp/WebpAnimationDecoder.cs | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 93ded17f82..09653fd4cd 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -315,27 +315,13 @@ private void AlphaBlend(ImageFrame src, ImageFrame dst, { Buffer2D srcPixels = src.PixelBuffer; Buffer2D dstPixels = dst.PixelBuffer; - Rgba32 srcRgba = default; - Rgba32 dstRgba = default; - PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); for (int y = frameY; y < frameY + frameHeight; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - for (int x = frameX; x < frameX + frameWidth; x++) - { - ref TPixel srcPixel = ref srcPixelRow[x]; - ref TPixel dstPixel = ref dstPixelRow[x]; - srcPixel.ToRgba32(ref srcRgba); - dstPixel.ToRgba32(ref dstRgba); - - if (srcRgba.A is not 0) - { - int dstFactorA = dstRgba.A * (255 - srcRgba.A) / 255; - Rgba32 blendResult = blender.Blend(srcRgba, dstRgba, 1.0f / dstFactorA); - dstPixel.FromRgba32(blendResult); - } - } + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); + + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); } }