diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index 173d9436dd..86489cd363 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.
///
@@ -57,19 +62,97 @@ internal class WebpAnimationDecoder : IDisposable
///
private readonly BackgroundColorHandling backgroundColorHandling;
+ ///
+ /// How to handle validation of errors in different segments of encoded image files.
+ ///
+ private readonly SegmentIntegrityHandling segmentIntegrityHandling;
+
///
/// Initializes a new instance of the class.
///
/// 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)
+ /// How to handle validation of errors in different segments of encoded image files.
+ public WebpAnimationDecoder(
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ uint maxFrames,
+ bool skipMetadata,
+ BackgroundColorHandling backgroundColorHandling,
+ SegmentIntegrityHandling segmentIntegrityHandling)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.maxFrames = maxFrames;
+ this.skipMetadata = skipMetadata;
this.backgroundColorHandling = backgroundColorHandling;
+ this.segmentIntegrityHandling = segmentIntegrityHandling;
+ }
+
+ ///
+ /// Reads the animated webp image information from the specified stream.
+ ///
+ /// 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 ImageInfo Identify(
+ BufferedReadStream stream,
+ WebpFeatures features,
+ uint width,
+ uint height,
+ uint completeDataSize)
+ {
+ List framesMetadata = [];
+ this.metadata = new ImageMetadata();
+ this.webpMetadata = this.metadata.GetWebpMetadata();
+ this.webpMetadata.RepeatCount = features.AnimationLoopCount;
+
+ Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
+ ? Color.Transparent
+ : features.AnimationBackgroundColor!.Value;
+
+ this.webpMetadata.BackgroundColor = backgroundColor;
+
+ 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, this.segmentIntegrityHandling, 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 Size((int)width, (int)height), this.metadata, framesMetadata);
}
///
@@ -128,10 +211,12 @@ internal class WebpAnimationDecoder : IDisposable
break;
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
- WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
+ WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, 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;
}
@@ -144,6 +229,26 @@ internal class WebpAnimationDecoder : IDisposable
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.
///
@@ -155,6 +260,7 @@ internal class WebpAnimationDecoder : IDisposable
/// The width of the image.
/// The height of the image.
/// The default background color of the canvas in.
+ /// The number of bytes read from the stream while parsing the frame information.
private uint ReadFrame(
BufferedReadStream stream,
ref Image? image,
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
index 8df159dbff..4ce64629fd 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;
@@ -120,6 +121,7 @@ internal static class WebpChunkParsingUtils
return new WebpImageInfo
{
+ DataSize = dataSize,
Width = width,
Height = height,
XScale = xScale,
@@ -178,6 +180,7 @@ internal static class WebpChunkParsingUtils
return new WebpImageInfo
{
+ DataSize = imageDataSize,
Width = width,
Height = height,
BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24,
@@ -333,7 +336,13 @@ internal static class WebpChunkParsingUtils
/// 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(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, Span buffer)
+ public static void ParseOptionalChunks(
+ BufferedReadStream stream,
+ WebpChunkType chunkType,
+ ImageMetadata metadata,
+ bool ignoreMetaData,
+ SegmentIntegrityHandling segmentIntegrityHandling,
+ Span buffer)
{
long streamLength = stream.Length;
while (stream.Position < streamLength)
@@ -353,12 +362,30 @@ internal static class WebpChunkParsingUtils
bytesRead = stream.Read(exifData, 0, (int)chunkLength);
if (bytesRead != chunkLength)
{
- WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
+ if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
+ }
+
+ return;
}
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;
@@ -367,7 +394,12 @@ internal static class WebpChunkParsingUtils
bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
if (bytesRead != chunkLength)
{
- WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
+ if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
+ }
+
+ return;
}
if (metadata.XmpProfile != null)
@@ -383,6 +415,16 @@ internal static class WebpChunkParsingUtils
}
}
+ 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 51379a32ae..0e9888adb2 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -56,6 +56,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
///
private readonly BackgroundColorHandling backgroundColorHandling;
+ private readonly SegmentIntegrityHandling segmentIntegrityHandling;
+
///
/// Initializes a new instance of the class.
///
@@ -64,6 +66,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
: base(options.GeneralOptions)
{
this.backgroundColorHandling = options.BackgroundColorHandling;
+ this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.configuration = options.GeneralOptions.Configuration;
this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.maxFrames = options.GeneralOptions.MaxFrames;
@@ -89,7 +92,10 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.memoryAllocator,
this.configuration,
this.maxFrames,
- this.backgroundColorHandling);
+ this.skipMetadata,
+ this.backgroundColorHandling,
+ this.segmentIntegrityHandling);
+
return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
@@ -101,6 +107,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.webImageInfo.Vp8LBitReader,
this.memoryAllocator,
this.configuration);
+
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
@@ -109,6 +116,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.webImageInfo.Vp8BitReader,
this.memoryAllocator,
this.configuration);
+
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}
@@ -131,11 +139,29 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
///
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,
+ this.segmentIntegrityHandling);
+
+ return animationDecoder.Identify(
+ stream,
+ this.webImageInfo.Features,
+ this.webImageInfo.Width,
+ this.webImageInfo.Height,
+ fileSize);
+ }
+
return new ImageInfo(
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
metadata);
@@ -211,6 +237,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
}
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)
{
@@ -273,7 +301,9 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.ReadAlphaData(stream, features, ignoreAlpha, buffer);
break;
default:
- WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
+
+ // Specification explicitly states to ignore unknown chunks.
+ // We do not support writing these chunks at present.
break;
}
@@ -335,7 +365,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
- // Ignore invalid chunk.
+ if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
+ }
+
return;
}
@@ -385,7 +419,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
- // Ignore invalid chunk.
+ if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
+ }
+
return;
}
diff --git a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
index 3428ce199a..e0993145f0 100644
--- a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
+++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
@@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Webp;
internal class WebpImageInfo : IDisposable
{
+ ///
+ /// Gets or sets the size of the encoded image data in bytes.
+ ///
+ public uint DataSize { get; set; }
+
///
/// Gets or sets the bitmap width in pixels.
///
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index 111544f7f5..c0abed214b 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -314,6 +314,21 @@ public class WebpDecoderTests
Assert.Equal(12, image.Frames.Count);
}
+ [Theory]
+ [InlineData(Lossless.Animated)]
+ public void Info_AnimatedLossless_VerifyAllFrames(string imagePath)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using MemoryStream stream = new(testFile.Bytes, false);
+ ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream);
+ WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
+ WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata();
+
+ Assert.Equal(0, webpMetaData.RepeatCount);
+ Assert.Equal(150U, frameMetaData.FrameDelay);
+ Assert.Equal(12, image.FrameCount);
+ }
+
[Theory]
[WithFile(Lossy.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider provider)
@@ -331,6 +346,21 @@ public class WebpDecoderTests
Assert.Equal(12, image.Frames.Count);
}
+ [Theory]
+ [InlineData(Lossy.Animated)]
+ public void Info_AnimatedLossy_VerifyAllFrames(string imagePath)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using MemoryStream stream = new(testFile.Bytes, false);
+ ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream);
+ WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
+ WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata();
+
+ Assert.Equal(0, webpMetaData.RepeatCount);
+ Assert.Equal(150U, frameMetaData.FrameDelay);
+ Assert.Equal(12, image.FrameCount);
+ }
+
[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame(TestImageProvider provider)