Skip to content

Commit 338fd4c

Browse files
Fix #2992
1 parent 52c73f3 commit 338fd4c

File tree

5 files changed

+234
-13
lines changed

5 files changed

+234
-13
lines changed

src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

Lines changed: 109 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>
@@ -57,19 +62,97 @@ internal class WebpAnimationDecoder : IDisposable
5762
/// </summary>
5863
private readonly BackgroundColorHandling backgroundColorHandling;
5964

65+
/// <summary>
66+
/// How to handle validation of errors in different segments of encoded image files.
67+
/// </summary>
68+
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
69+
6070
/// <summary>
6171
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
6272
/// </summary>
6373
/// <param name="memoryAllocator">The memory allocator.</param>
6474
/// <param name="configuration">The global configuration.</param>
6575
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
76+
/// <param name="skipMetadata">Whether to skip metadata.</param>
6677
/// <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)
78+
/// <param name="segmentIntegrityHandling">How to handle validation of errors in different segments of encoded image files.</param>
79+
public WebpAnimationDecoder(
80+
MemoryAllocator memoryAllocator,
81+
Configuration configuration,
82+
uint maxFrames,
83+
bool skipMetadata,
84+
BackgroundColorHandling backgroundColorHandling,
85+
SegmentIntegrityHandling segmentIntegrityHandling)
6886
{
6987
this.memoryAllocator = memoryAllocator;
7088
this.configuration = configuration;
7189
this.maxFrames = maxFrames;
90+
this.skipMetadata = skipMetadata;
7291
this.backgroundColorHandling = backgroundColorHandling;
92+
this.segmentIntegrityHandling = segmentIntegrityHandling;
93+
}
94+
95+
/// <summary>
96+
/// Reads the animated webp image information from the specified stream.
97+
/// </summary>
98+
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
99+
/// <param name="features">The webp features.</param>
100+
/// <param name="width">The width of the image.</param>
101+
/// <param name="height">The height of the image.</param>
102+
/// <param name="completeDataSize">The size of the image data in bytes.</param>
103+
public ImageInfo Identify(
104+
BufferedReadStream stream,
105+
WebpFeatures features,
106+
uint width,
107+
uint height,
108+
uint completeDataSize)
109+
{
110+
List<ImageFrameMetadata> framesMetadata = [];
111+
this.metadata = new ImageMetadata();
112+
this.webpMetadata = this.metadata.GetWebpMetadata();
113+
this.webpMetadata.RepeatCount = features.AnimationLoopCount;
114+
115+
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
116+
? Color.Transparent
117+
: features.AnimationBackgroundColor!.Value;
118+
119+
this.webpMetadata.BackgroundColor = backgroundColor;
120+
121+
Span<byte> buffer = stackalloc byte[4];
122+
uint frameCount = 0;
123+
int remainingBytes = (int)completeDataSize;
124+
while (remainingBytes > 0)
125+
{
126+
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
127+
remainingBytes -= 4;
128+
switch (chunkType)
129+
{
130+
case WebpChunkType.FrameData:
131+
132+
ImageFrameMetadata frameMetadata = new();
133+
uint dataSize = ReadFrameInfo(stream, ref frameMetadata);
134+
framesMetadata.Add(frameMetadata);
135+
136+
remainingBytes -= (int)dataSize;
137+
break;
138+
case WebpChunkType.Xmp:
139+
case WebpChunkType.Exif:
140+
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
141+
break;
142+
default:
143+
144+
// Specification explicitly states to ignore unknown chunks.
145+
// We do not support writing these chunks at present.
146+
break;
147+
}
148+
149+
if (stream.Position == stream.Length || ++frameCount == this.maxFrames)
150+
{
151+
break;
152+
}
153+
}
154+
155+
return new ImageInfo(new Size((int)width, (int)height), this.metadata, framesMetadata);
73156
}
74157

75158
/// <summary>
@@ -128,10 +211,12 @@ public Image<TPixel> Decode<TPixel>(
128211
break;
129212
case WebpChunkType.Xmp:
130213
case WebpChunkType.Exif:
131-
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
214+
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
132215
break;
133216
default:
134-
WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
217+
218+
// Specification explicitly states to ignore unknown chunks.
219+
// We do not support writing these chunks at present.
135220
break;
136221
}
137222

@@ -144,6 +229,26 @@ public Image<TPixel> Decode<TPixel>(
144229
return image!;
145230
}
146231

232+
/// <summary>
233+
/// Reads frame information from the specified stream and updates the provided frame metadata.
234+
/// </summary>
235+
/// <param name="stream">The stream from which to read the frame information. Must support reading and seeking.</param>
236+
/// <param name="frameMetadata">A reference to the structure that will be updated with the parsed frame metadata.</param>
237+
/// <returns>The number of bytes read from the stream while parsing the frame information.</returns>
238+
private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata)
239+
{
240+
WebpFrameData frameData = WebpFrameData.Parse(stream);
241+
SetFrameMetadata(frameMetadata, frameData);
242+
243+
// Size of the frame header chunk.
244+
const int chunkHeaderSize = 16;
245+
246+
uint remaining = frameData.DataSize - chunkHeaderSize;
247+
stream.Skip((int)remaining);
248+
249+
return remaining;
250+
}
251+
147252
/// <summary>
148253
/// Reads an individual webp frame.
149254
/// </summary>
@@ -155,6 +260,7 @@ public Image<TPixel> Decode<TPixel>(
155260
/// <param name="width">The width of the image.</param>
156261
/// <param name="height">The height of the image.</param>
157262
/// <param name="backgroundColor">The default background color of the canvas in.</param>
263+
/// <returns>The number of bytes read from the stream while parsing the frame information.</returns>
158264
private uint ReadFrame<TPixel>(
159265
BufferedReadStream stream,
160266
ref Image<TPixel>? image,

src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

Lines changed: 46 additions & 4 deletions
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;
@@ -120,6 +121,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe
120121

121122
return new WebpImageInfo
122123
{
124+
DataSize = dataSize,
123125
Width = width,
124126
Height = height,
125127
XScale = xScale,
@@ -178,6 +180,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff
178180

179181
return new WebpImageInfo
180182
{
183+
DataSize = imageDataSize,
181184
Width = width,
182185
Height = height,
183186
BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24,
@@ -333,7 +336,13 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte>
333336
/// If there are more such chunks, readers MAY ignore all except the first one.
334337
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
335338
/// </summary>
336-
public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, Span<byte> buffer)
339+
public static void ParseOptionalChunks(
340+
BufferedReadStream stream,
341+
WebpChunkType chunkType,
342+
ImageMetadata metadata,
343+
bool ignoreMetaData,
344+
SegmentIntegrityHandling segmentIntegrityHandling,
345+
Span<byte> buffer)
337346
{
338347
long streamLength = stream.Length;
339348
while (stream.Position < streamLength)
@@ -353,12 +362,30 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
353362
bytesRead = stream.Read(exifData, 0, (int)chunkLength);
354363
if (bytesRead != chunkLength)
355364
{
356-
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
365+
if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
366+
{
367+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
368+
}
369+
370+
return;
357371
}
358372

359373
if (metadata.ExifProfile != null)
360374
{
361-
metadata.ExifProfile = new ExifProfile(exifData);
375+
ExifProfile exifProfile = new(exifData);
376+
377+
// Set the resolution from the metadata.
378+
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
379+
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
380+
381+
if (horizontalValue > 0 && verticalValue > 0)
382+
{
383+
metadata.HorizontalResolution = horizontalValue;
384+
metadata.VerticalResolution = verticalValue;
385+
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
386+
}
387+
388+
metadata.ExifProfile = exifProfile;
362389
}
363390

364391
break;
@@ -367,7 +394,12 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
367394
bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
368395
if (bytesRead != chunkLength)
369396
{
370-
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
397+
if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
398+
{
399+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
400+
}
401+
402+
return;
371403
}
372404

373405
if (metadata.XmpProfile != null)
@@ -383,6 +415,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
383415
}
384416
}
385417

418+
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
419+
{
420+
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))
421+
{
422+
return resolution.Value.ToDouble();
423+
}
424+
425+
return 0;
426+
}
427+
386428
/// <summary>
387429
/// Determines if the chunk type is an optional VP8X chunk.
388430
/// </summary>

src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
5656
/// </summary>
5757
private readonly BackgroundColorHandling backgroundColorHandling;
5858

59+
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
60+
5961
/// <summary>
6062
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
6163
/// </summary>
@@ -64,6 +66,7 @@ public WebpDecoderCore(WebpDecoderOptions options)
6466
: base(options.GeneralOptions)
6567
{
6668
this.backgroundColorHandling = options.BackgroundColorHandling;
69+
this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
6770
this.configuration = options.GeneralOptions.Configuration;
6871
this.skipMetadata = options.GeneralOptions.SkipMetadata;
6972
this.maxFrames = options.GeneralOptions.MaxFrames;
@@ -89,7 +92,10 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
8992
this.memoryAllocator,
9093
this.configuration,
9194
this.maxFrames,
92-
this.backgroundColorHandling);
95+
this.skipMetadata,
96+
this.backgroundColorHandling,
97+
this.segmentIntegrityHandling);
98+
9399
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
94100
}
95101

@@ -101,6 +107,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
101107
this.webImageInfo.Vp8LBitReader,
102108
this.memoryAllocator,
103109
this.configuration);
110+
104111
losslessDecoder.Decode(pixels, image.Width, image.Height);
105112
}
106113
else
@@ -109,6 +116,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
109116
this.webImageInfo.Vp8BitReader,
110117
this.memoryAllocator,
111118
this.configuration);
119+
112120
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
113121
}
114122

@@ -131,11 +139,29 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
131139
/// <inheritdoc />
132140
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
133141
{
134-
ReadImageHeader(stream, stackalloc byte[4]);
135-
142+
uint fileSize = ReadImageHeader(stream, stackalloc byte[4]);
136143
ImageMetadata metadata = new();
144+
137145
using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true))
138146
{
147+
if (this.webImageInfo.Features is { Animation: true })
148+
{
149+
using WebpAnimationDecoder animationDecoder = new(
150+
this.memoryAllocator,
151+
this.configuration,
152+
this.maxFrames,
153+
this.skipMetadata,
154+
this.backgroundColorHandling,
155+
this.segmentIntegrityHandling);
156+
157+
return animationDecoder.Identify(
158+
stream,
159+
this.webImageInfo.Features,
160+
this.webImageInfo.Width,
161+
this.webImageInfo.Height,
162+
fileSize);
163+
}
164+
139165
return new ImageInfo(
140166
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
141167
metadata);
@@ -211,6 +237,8 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad
211237
}
212238
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
213239
{
240+
// ANIM chunks appear before EXIF and XMP chunks.
241+
// Return after parsing an ANIM chunk - The animated decoder will handle the rest.
214242
bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer);
215243
if (isAnimationChunk)
216244
{
@@ -273,7 +301,9 @@ private bool ParseOptionalExtendedChunks(
273301
this.ReadAlphaData(stream, features, ignoreAlpha, buffer);
274302
break;
275303
default:
276-
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
304+
305+
// Specification explicitly states to ignore unknown chunks.
306+
// We do not support writing these chunks at present.
277307
break;
278308
}
279309

@@ -335,7 +365,11 @@ private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata,
335365
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
336366
if (bytesRead != exifChunkSize)
337367
{
338-
// Ignore invalid chunk.
368+
if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
369+
{
370+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
371+
}
372+
339373
return;
340374
}
341375

@@ -385,7 +419,11 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, S
385419
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
386420
if (bytesRead != xmpChunkSize)
387421
{
388-
// Ignore invalid chunk.
422+
if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
423+
{
424+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
425+
}
426+
389427
return;
390428
}
391429

src/ImageSharp/Formats/Webp/WebpImageInfo.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Webp;
88

99
internal class WebpImageInfo : IDisposable
1010
{
11+
/// <summary>
12+
/// Gets or sets the size of the encoded image data in bytes.
13+
/// </summary>
14+
public uint DataSize { get; set; }
15+
1116
/// <summary>
1217
/// Gets or sets the bitmap width in pixels.
1318
/// </summary>

0 commit comments

Comments
 (0)