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

Filter by extension

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
26 changes: 25 additions & 1 deletion 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 @@ -345,7 +346,20 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType

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 @@ -370,6 +384,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