Skip to content

Commit 44f5af1

Browse files
Merge pull request #3001 from SixLabors/js/fix-2992
v4 : Add Full Metadata Parsing for WEBP Animations
2 parents 52c73f3 + 302a280 commit 44f5af1

File tree

6 files changed

+237
-19
lines changed

6 files changed

+237
-19
lines changed

src/ImageSharp/Formats/Png/PngFrameMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private PngFrameMetadata(PngFrameMetadata other)
3232

3333
/// <summary>
3434
/// Gets or sets the frame delay for animated images.
35-
/// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to
35+
/// If not 0, when utilized in Png animation, this field specifies the number of seconds to
3636
/// wait before continuing with the processing of the Data Stream.
3737
/// The clock starts ticking immediately after the graphic is rendered.
3838
/// </summary>

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: 48 additions & 9 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

359-
if (metadata.ExifProfile != null)
373+
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,14 +394,16 @@ 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");
371-
}
397+
if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
398+
{
399+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
400+
}
372401

373-
if (metadata.XmpProfile != null)
374-
{
375-
metadata.XmpProfile = new XmpProfile(xmpData);
402+
return;
376403
}
377404

405+
metadata.XmpProfile ??= new XmpProfile(xmpData);
406+
378407
break;
379408
default:
380409
stream.Skip((int)chunkLength);
@@ -383,6 +412,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
383412
}
384413
}
385414

415+
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
416+
{
417+
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))
418+
{
419+
return resolution.Value.ToDouble();
420+
}
421+
422+
return 0;
423+
}
424+
386425
/// <summary>
387426
/// Determines if the chunk type is an optional VP8X chunk.
388427
/// </summary>

0 commit comments

Comments
 (0)