Skip to content

Commit 9443543

Browse files
Fix #2992
1 parent f4a2684 commit 9443543

File tree

4 files changed

+181
-6
lines changed

4 files changed

+181
-6
lines changed

src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ internal class WebpAnimationDecoder : IDisposable
3232
/// </summary>
3333
private readonly uint maxFrames;
3434

35+
/// <summary>
36+
/// Whether to skip metadata.
37+
/// </summary>
38+
private readonly bool skipMetadata;
39+
3540
/// <summary>
3641
/// The area to restore.
3742
/// </summary>
@@ -63,15 +68,85 @@ internal class WebpAnimationDecoder : IDisposable
6368
/// <param name="memoryAllocator">The memory allocator.</param>
6469
/// <param name="configuration">The global configuration.</param>
6570
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
71+
/// <param name="skipMetadata">Whether to skip metadata.</param>
6672
/// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param>
67-
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling)
73+
public WebpAnimationDecoder(
74+
MemoryAllocator memoryAllocator,
75+
Configuration configuration,
76+
uint maxFrames,
77+
bool skipMetadata,
78+
BackgroundColorHandling backgroundColorHandling)
6879
{
6980
this.memoryAllocator = memoryAllocator;
7081
this.configuration = configuration;
7182
this.maxFrames = maxFrames;
83+
this.skipMetadata = skipMetadata;
7284
this.backgroundColorHandling = backgroundColorHandling;
7385
}
7486

87+
/// <summary>
88+
/// Reads the animated webp image information from the specified stream.
89+
/// </summary>
90+
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
91+
/// <param name="bitsPerPixel">The bits per pixel.</param>
92+
/// <param name="features">The webp features.</param>
93+
/// <param name="width">The width of the image.</param>
94+
/// <param name="height">The height of the image.</param>
95+
/// <param name="completeDataSize">The size of the image data in bytes.</param>
96+
public ImageInfo Identify(
97+
BufferedReadStream stream,
98+
int bitsPerPixel,
99+
WebpFeatures features,
100+
uint width,
101+
uint height,
102+
uint completeDataSize)
103+
{
104+
List<ImageFrameMetadata> framesMetadata = new();
105+
this.metadata = new ImageMetadata();
106+
this.webpMetadata = this.metadata.GetWebpMetadata();
107+
this.webpMetadata.RepeatCount = features.AnimationLoopCount;
108+
109+
this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
110+
? Color.Transparent
111+
: features.AnimationBackgroundColor!.Value;
112+
113+
Span<byte> buffer = stackalloc byte[4];
114+
uint frameCount = 0;
115+
int remainingBytes = (int)completeDataSize;
116+
while (remainingBytes > 0)
117+
{
118+
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
119+
remainingBytes -= 4;
120+
switch (chunkType)
121+
{
122+
case WebpChunkType.FrameData:
123+
124+
ImageFrameMetadata frameMetadata = new();
125+
uint dataSize = ReadFrameInfo(stream, ref frameMetadata);
126+
framesMetadata.Add(frameMetadata);
127+
128+
remainingBytes -= (int)dataSize;
129+
break;
130+
case WebpChunkType.Xmp:
131+
case WebpChunkType.Exif:
132+
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, buffer);
133+
break;
134+
default:
135+
136+
// Specification explicitly states to ignore unknown chunks.
137+
// We do not support writing these chunks at present.
138+
break;
139+
}
140+
141+
if (stream.Position == stream.Length || ++frameCount == this.maxFrames)
142+
{
143+
break;
144+
}
145+
}
146+
147+
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new Size((int)width, (int)height), this.metadata, framesMetadata);
148+
}
149+
75150
/// <summary>
76151
/// Decodes the animated webp image from the specified stream.
77152
/// </summary>
@@ -127,10 +202,12 @@ public Image<TPixel> Decode<TPixel>(
127202
break;
128203
case WebpChunkType.Xmp:
129204
case WebpChunkType.Exif:
130-
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
205+
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, buffer);
131206
break;
132207
default:
133-
WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
208+
209+
// Specification explicitly states to ignore unknown chunks.
210+
// We do not support writing these chunks at present.
134211
break;
135212
}
136213

@@ -143,6 +220,26 @@ public Image<TPixel> Decode<TPixel>(
143220
return image!;
144221
}
145222

223+
/// <summary>
224+
/// Reads frame information from the specified stream and updates the provided frame metadata.
225+
/// </summary>
226+
/// <param name="stream">The stream from which to read the frame information. Must support reading and seeking.</param>
227+
/// <param name="frameMetadata">A reference to the structure that will be updated with the parsed frame metadata.</param>
228+
/// <returns>The number of bytes read from the stream while parsing the frame information.</returns>
229+
private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata)
230+
{
231+
WebpFrameData frameData = WebpFrameData.Parse(stream);
232+
SetFrameMetadata(frameMetadata, frameData);
233+
234+
// Size of the frame header chunk.
235+
const int chunkHeaderSize = 16;
236+
237+
uint remaining = frameData.DataSize - chunkHeaderSize;
238+
stream.Skip((int)remaining);
239+
240+
return remaining;
241+
}
242+
146243
/// <summary>
147244
/// Reads an individual webp frame.
148245
/// </summary>

src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Buffers.Binary;
5+
using SixLabors.ImageSharp.Common.Helpers;
56
using SixLabors.ImageSharp.Formats.Webp.BitReader;
67
using SixLabors.ImageSharp.Formats.Webp.Lossy;
78
using SixLabors.ImageSharp.IO;
@@ -345,7 +346,20 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
345346

346347
if (metadata.ExifProfile != null)
347348
{
348-
metadata.ExifProfile = new ExifProfile(exifData);
349+
ExifProfile exifProfile = new(exifData);
350+
351+
// Set the resolution from the metadata.
352+
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
353+
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
354+
355+
if (horizontalValue > 0 && verticalValue > 0)
356+
{
357+
metadata.HorizontalResolution = horizontalValue;
358+
metadata.VerticalResolution = verticalValue;
359+
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
360+
}
361+
362+
metadata.ExifProfile = exifProfile;
349363
}
350364

351365
break;
@@ -370,6 +384,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
370384
}
371385
}
372386

387+
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
388+
{
389+
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))
390+
{
391+
return resolution.Value.ToDouble();
392+
}
393+
394+
return 0;
395+
}
396+
373397
/// <summary>
374398
/// Determines if the chunk type is an optional VP8X chunk.
375399
/// </summary>

src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
8989
this.memoryAllocator,
9090
this.configuration,
9191
this.maxFrames,
92+
this.skipMetadata,
9293
this.backgroundColorHandling);
94+
9395
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
9496
}
9597

@@ -101,6 +103,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
101103
this.webImageInfo.Vp8LBitReader,
102104
this.memoryAllocator,
103105
this.configuration);
106+
104107
losslessDecoder.Decode(pixels, image.Width, image.Height);
105108
}
106109
else
@@ -109,6 +112,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
109112
this.webImageInfo.Vp8BitReader,
110113
this.memoryAllocator,
111114
this.configuration);
115+
112116
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
113117
}
114118

@@ -131,11 +135,29 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
131135
/// <inheritdoc />
132136
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
133137
{
134-
ReadImageHeader(stream, stackalloc byte[4]);
135-
138+
uint fileSize = ReadImageHeader(stream, stackalloc byte[4]);
136139
ImageMetadata metadata = new();
140+
137141
using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true))
138142
{
143+
if (this.webImageInfo.Features is { Animation: true })
144+
{
145+
using WebpAnimationDecoder animationDecoder = new(
146+
this.memoryAllocator,
147+
this.configuration,
148+
this.maxFrames,
149+
this.skipMetadata,
150+
this.backgroundColorHandling);
151+
152+
return animationDecoder.Identify(
153+
stream,
154+
(int)this.webImageInfo.BitsPerPixel,
155+
this.webImageInfo.Features,
156+
this.webImageInfo.Width,
157+
this.webImageInfo.Height,
158+
fileSize);
159+
}
160+
139161
return new ImageInfo(
140162
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
141163
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
@@ -208,6 +230,8 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad
208230
}
209231
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
210232
{
233+
// ANIM chunks appear before EXIF and XMP chunks.
234+
// Return after parsing an ANIM chunk - The animated decoder will handle the rest.
211235
bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer);
212236
if (isAnimationChunk)
213237
{

tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,21 @@ public void Decode_AnimatedLossless_VerifyAllFrames<TPixel>(TestImageProvider<TP
314314
Assert.Equal(12, image.Frames.Count);
315315
}
316316

317+
[Theory]
318+
[InlineData(Lossless.Animated)]
319+
public void Info_AnimatedLossless_VerifyAllFrames(string imagePath)
320+
{
321+
TestFile testFile = TestFile.Create(imagePath);
322+
using MemoryStream stream = new(testFile.Bytes, false);
323+
ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream);
324+
WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
325+
WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata();
326+
327+
Assert.Equal(0, webpMetaData.RepeatCount);
328+
Assert.Equal(150U, frameMetaData.FrameDelay);
329+
Assert.Equal(12, image.FrameMetadataCollection.Count);
330+
}
331+
317332
[Theory]
318333
[WithFile(Lossy.Animated, PixelTypes.Rgba32)]
319334
public void Decode_AnimatedLossy_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
@@ -331,6 +346,21 @@ public void Decode_AnimatedLossy_VerifyAllFrames<TPixel>(TestImageProvider<TPixe
331346
Assert.Equal(12, image.Frames.Count);
332347
}
333348

349+
[Theory]
350+
[InlineData(Lossy.Animated)]
351+
public void Info_AnimatedLossy_VerifyAllFrames(string imagePath)
352+
{
353+
TestFile testFile = TestFile.Create(imagePath);
354+
using MemoryStream stream = new(testFile.Bytes, false);
355+
ImageInfo image = WebpDecoder.Instance.Identify(DecoderOptions.Default, stream);
356+
WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
357+
WebpFrameMetadata frameMetaData = image.FrameMetadataCollection[0].GetWebpMetadata();
358+
359+
Assert.Equal(0, webpMetaData.RepeatCount);
360+
Assert.Equal(150U, frameMetaData.FrameDelay);
361+
Assert.Equal(12, image.FrameMetadataCollection.Count);
362+
}
363+
334364
[Theory]
335365
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
336366
public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame<TPixel>(TestImageProvider<TPixel> provider)

0 commit comments

Comments
 (0)