diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index dbda4d73c9..c88c23ecb5 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -30,7 +30,7 @@ private PngFrameMetadata(PngFrameMetadata other) /// /// Gets or sets the frame delay for animated images. - /// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to + /// If not 0, when utilized in Png animation, this field specifies the number of seconds to /// wait before continuing with the processing of the Data Stream. /// The clock starts ticking immediately after the graphic is rendered. /// diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index b9f58c3d84..359b380b22 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -32,6 +32,11 @@ internal class WebpAnimationDecoder : IDisposable /// private readonly uint maxFrames; + /// + /// Whether to skip metadata. + /// + private readonly bool skipMetadata; + /// /// The area to restore. /// @@ -63,15 +68,85 @@ internal class WebpAnimationDecoder : IDisposable /// The memory allocator. /// The global configuration. /// The maximum number of frames to decode. Inclusive. + /// Whether to skip metadata. /// The flag to decide how to handle the background color in the Animation Chunk. - public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling) + public WebpAnimationDecoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + uint maxFrames, + bool skipMetadata, + BackgroundColorHandling backgroundColorHandling) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.maxFrames = maxFrames; + this.skipMetadata = skipMetadata; this.backgroundColorHandling = backgroundColorHandling; } + /// + /// Reads the animated webp image information from the specified stream. + /// + /// The stream, where the image should be decoded from. Cannot be null. + /// The bits per pixel. + /// The webp features. + /// The width of the image. + /// The height of the image. + /// The size of the image data in bytes. + public ImageInfo Identify( + BufferedReadStream stream, + int bitsPerPixel, + WebpFeatures features, + uint width, + uint height, + uint completeDataSize) + { + List framesMetadata = new(); + this.metadata = new ImageMetadata(); + this.webpMetadata = this.metadata.GetWebpMetadata(); + this.webpMetadata.RepeatCount = features.AnimationLoopCount; + + this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? Color.Transparent + : features.AnimationBackgroundColor!.Value; + + Span buffer = stackalloc byte[4]; + uint frameCount = 0; + int remainingBytes = (int)completeDataSize; + while (remainingBytes > 0) + { + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + remainingBytes -= 4; + switch (chunkType) + { + case WebpChunkType.FrameData: + + ImageFrameMetadata frameMetadata = new(); + uint dataSize = ReadFrameInfo(stream, ref frameMetadata); + framesMetadata.Add(frameMetadata); + + remainingBytes -= (int)dataSize; + break; + case WebpChunkType.Xmp: + case WebpChunkType.Exif: + WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, buffer); + break; + default: + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. + break; + } + + if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + { + break; + } + } + + return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new Size((int)width, (int)height), this.metadata, framesMetadata); + } + /// /// Decodes the animated webp image from the specified stream. /// @@ -127,10 +202,12 @@ public Image Decode( break; case WebpChunkType.Xmp: case WebpChunkType.Exif: - WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer); + WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, buffer); break; default: - WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data"); + + // Specification explicitly states to ignore unknown chunks. + // We do not support writing these chunks at present. break; } @@ -143,6 +220,26 @@ public Image Decode( return image!; } + /// + /// Reads frame information from the specified stream and updates the provided frame metadata. + /// + /// The stream from which to read the frame information. Must support reading and seeking. + /// A reference to the structure that will be updated with the parsed frame metadata. + /// The number of bytes read from the stream while parsing the frame information. + private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata) + { + WebpFrameData frameData = WebpFrameData.Parse(stream); + SetFrameMetadata(frameMetadata, frameData); + + // Size of the frame header chunk. + const int chunkHeaderSize = 16; + + uint remaining = frameData.DataSize - chunkHeaderSize; + stream.Skip((int)remaining); + + return remaining; + } + /// /// Reads an individual webp frame. /// diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 839798b4d7..6926928ea9 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -343,9 +344,22 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); } - if (metadata.ExifProfile != null) + if (metadata.ExifProfile == null) { - metadata.ExifProfile = new ExifProfile(exifData); + ExifProfile exifProfile = new(exifData); + + // Set the resolution from the metadata. + double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution); + double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution); + + if (horizontalValue > 0 && verticalValue > 0) + { + metadata.HorizontalResolution = horizontalValue; + metadata.VerticalResolution = verticalValue; + metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile); + } + + metadata.ExifProfile = exifProfile; } break; @@ -357,10 +371,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); } - if (metadata.XmpProfile != null) - { - metadata.XmpProfile = new XmpProfile(xmpData); - } + metadata.XmpProfile ??= new XmpProfile(xmpData); break; default: @@ -370,6 +381,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType } } + private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag) + { + if (exifProfile.TryGetValue(tag, out IExifValue? resolution)) + { + return resolution.Value.ToDouble(); + } + + return 0; + } + /// /// Determines if the chunk type is an optional VP8X chunk. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 21e1b55cfc..362677eeee 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -89,7 +89,9 @@ protected override Image Decode(BufferedReadStream stream, Cance this.memoryAllocator, this.configuration, this.maxFrames, + this.skipMetadata, this.backgroundColorHandling); + return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -101,6 +103,7 @@ protected override Image Decode(BufferedReadStream stream, Cance this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + losslessDecoder.Decode(pixels, image.Width, image.Height); } else @@ -109,6 +112,7 @@ protected override Image Decode(BufferedReadStream stream, Cance this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -131,11 +135,29 @@ protected override Image Decode(BufferedReadStream stream, Cance /// protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - ReadImageHeader(stream, stackalloc byte[4]); - + uint fileSize = ReadImageHeader(stream, stackalloc byte[4]); ImageMetadata metadata = new(); + using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { + if (this.webImageInfo.Features is { Animation: true }) + { + using WebpAnimationDecoder animationDecoder = new( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.skipMetadata, + this.backgroundColorHandling); + + return animationDecoder.Identify( + stream, + (int)this.webImageInfo.BitsPerPixel, + this.webImageInfo.Features, + this.webImageInfo.Width, + this.webImageInfo.Height, + fileSize); + } + return new ImageInfo( new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), @@ -208,6 +230,8 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { + // ANIM chunks appear before EXIF and XMP chunks. + // Return after parsing an ANIM chunk - The animated decoder will handle the rest. bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c8d780cc20..cb4bc5ca90 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -314,6 +314,21 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider(TestImageProvider provider) @@ -331,6 +346,21 @@ public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider(TestImageProvider provider)