Browse Source

Merge pull request #3001 from SixLabors/js/fix-2992

v4 : Add Full Metadata Parsing for WEBP Animations
pull/3003/head
James Jackson-South 7 months ago
committed by GitHub
parent
commit
44f5af1354
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  2. 112
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  3. 57
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  4. 50
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  5. 5
      src/ImageSharp/Formats/Webp/WebpImageInfo.cs
  6. 30
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

2
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -32,7 +32,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
/// <summary>
/// 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.
/// </summary>

112
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -32,6 +32,11 @@ internal class WebpAnimationDecoder : IDisposable
/// </summary>
private readonly uint maxFrames;
/// <summary>
/// Whether to skip metadata.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// The area to restore.
/// </summary>
@ -57,19 +62,97 @@ internal class WebpAnimationDecoder : IDisposable
/// </summary>
private readonly BackgroundColorHandling backgroundColorHandling;
/// <summary>
/// How to handle validation of errors in different segments of encoded image files.
/// </summary>
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary>
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
/// <param name="skipMetadata">Whether to skip metadata.</param>
/// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param>
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling)
/// <param name="segmentIntegrityHandling">How to handle validation of errors in different segments of encoded image files.</param>
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;
}
/// <summary>
/// Reads the animated webp image information from the specified stream.
/// </summary>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="features">The webp features.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="completeDataSize">The size of the image data in bytes.</param>
public ImageInfo Identify(
BufferedReadStream stream,
WebpFeatures features,
uint width,
uint height,
uint completeDataSize)
{
List<ImageFrameMetadata> 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<byte> 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);
}
/// <summary>
@ -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!;
}
/// <summary>
/// Reads frame information from the specified stream and updates the provided frame metadata.
/// </summary>
/// <param name="stream">The stream from which to read the frame information. Must support reading and seeking.</param>
/// <param name="frameMetadata">A reference to the structure that will be updated with the parsed frame metadata.</param>
/// <returns>The number of bytes read from the stream while parsing the frame information.</returns>
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;
}
/// <summary>
/// Reads an individual webp frame.
/// </summary>
@ -155,6 +260,7 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="backgroundColor">The default background color of the canvas in.</param>
/// <returns>The number of bytes read from the stream while parsing the frame information.</returns>
private uint ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,

57
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.
/// </summary>
public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, Span<byte> buffer)
public static void ParseOptionalChunks(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata metadata,
bool ignoreMetaData,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> 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)
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,14 +394,16 @@ 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");
}
if (metadata.XmpProfile != null)
{
metadata.XmpProfile = new XmpProfile(xmpData);
return;
}
metadata.XmpProfile ??= new XmpProfile(xmpData);
break;
default:
stream.Skip((int)chunkLength);
@ -383,6 +412,16 @@ internal static class WebpChunkParsingUtils
}
}
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
{
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))
{
return resolution.Value.ToDouble();
}
return 0;
}
/// <summary>
/// Determines if the chunk type is an optional VP8X chunk.
/// </summary>

50
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -56,6 +56,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
/// </summary>
private readonly BackgroundColorHandling backgroundColorHandling;
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
/// </summary>
@ -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<TPixel>(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
/// <inheritdoc />
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;
}

5
src/ImageSharp/Formats/Webp/WebpImageInfo.cs

@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Webp;
internal class WebpImageInfo : IDisposable
{
/// <summary>
/// Gets or sets the size of the encoded image data in bytes.
/// </summary>
public uint DataSize { get; set; }
/// <summary>
/// Gets or sets the bitmap width in pixels.
/// </summary>

30
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<TPixel>(TestImageProvider<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)

Loading…
Cancel
Save