Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Png/PngFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private PngFrameMetadata(PngFrameMetadata other)

/// <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>
Expand Down
103 changes: 100 additions & 3 deletions src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -63,15 +68,85 @@ internal class WebpAnimationDecoder : IDisposable
/// <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)
public WebpAnimationDecoder(
MemoryAllocator memoryAllocator,
Configuration configuration,
uint maxFrames,
bool skipMetadata,
BackgroundColorHandling backgroundColorHandling)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.maxFrames = maxFrames;
this.skipMetadata = skipMetadata;
this.backgroundColorHandling = backgroundColorHandling;
}

/// <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="bitsPerPixel">The bits per pixel.</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,
int bitsPerPixel,
WebpFeatures features,
uint width,
uint height,
uint completeDataSize)
{
List<ImageFrameMetadata> framesMetadata = new();
this.metadata = new ImageMetadata();
this.webpMetadata = this.metadata.GetWebpMetadata();
this.webpMetadata.RepeatCount = features.AnimationLoopCount;

this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? Color.Transparent
: features.AnimationBackgroundColor!.Value;

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, 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 PixelTypeInfo(bitsPerPixel), new Size((int)width, (int)height), this.metadata, framesMetadata);
}

/// <summary>
/// Decodes the animated webp image from the specified stream.
/// </summary>
Expand Down Expand Up @@ -127,10 +202,12 @@ public Image<TPixel> Decode<TPixel>(
break;
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, 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;
}

Expand All @@ -143,6 +220,26 @@ public Image<TPixel> Decode<TPixel>(
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>
Expand Down
33 changes: 27 additions & 6 deletions src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -343,9 +344,22 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}

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;
Expand All @@ -357,10 +371,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}

if (metadata.XmpProfile != null)
{
metadata.XmpProfile = new XmpProfile(xmpData);
}
metadata.XmpProfile ??= new XmpProfile(xmpData);

break;
default:
Expand All @@ -370,6 +381,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
}
}

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>
Expand Down
28 changes: 26 additions & 2 deletions src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
this.memoryAllocator,
this.configuration,
this.maxFrames,
this.skipMetadata,
this.backgroundColorHandling);

return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}

Expand All @@ -101,6 +103,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
this.webImageInfo.Vp8LBitReader,
this.memoryAllocator,
this.configuration);

losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
Expand All @@ -109,6 +112,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
this.webImageInfo.Vp8BitReader,
this.memoryAllocator,
this.configuration);

lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}

Expand All @@ -131,11 +135,29 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
/// <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);

return animationDecoder.Identify(
stream,
(int)this.webImageInfo.BitsPerPixel,
this.webImageInfo.Features,
this.webImageInfo.Width,
this.webImageInfo.Height,
fileSize);
}

return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
Expand Down Expand Up @@ -208,6 +230,8 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad
}
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)
{
Expand Down
30 changes: 30 additions & 0 deletions tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,21 @@ public void Decode_AnimatedLossless_VerifyAllFrames<TPixel>(TestImageProvider<TP
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.FrameMetadataCollection.Count);
}

[Theory]
[WithFile(Lossy.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
Expand All @@ -331,6 +346,21 @@ public void Decode_AnimatedLossy_VerifyAllFrames<TPixel>(TestImageProvider<TPixe
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.FrameMetadataCollection.Count);
}

[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame<TPixel>(TestImageProvider<TPixel> provider)
Expand Down
Loading