Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -32,7 +32,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
112 changes: 109 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 All @@ -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>
Expand Down Expand Up @@ -128,10 +211,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, 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;
}

Expand All @@ -144,6 +229,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 All @@ -155,6 +260,7 @@ public Image<TPixel> Decode<TPixel>(
/// <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,
Expand Down
57 changes: 48 additions & 9 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 @@ -120,6 +121,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe

return new WebpImageInfo
{
DataSize = dataSize,
Width = width,
Height = height,
XScale = xScale,
Expand Down Expand Up @@ -178,6 +180,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff

return new WebpImageInfo
{
DataSize = imageDataSize,
Width = width,
Height = height,
BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24,
Expand Down Expand Up @@ -333,7 +336,13 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte>
/// 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)
Expand All @@ -353,12 +362,30 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
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;
Expand All @@ -367,14 +394,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
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);
Expand All @@ -383,6 +412,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
Loading
Loading