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)