Skip to content

Commit 703d1ba

Browse files
Merge pull request #3101 from andreas-eriksson/ImageInfo.FrameMetadataCollection-not-populated-correctly-for-animated-png-images
Fix Identify returning incorrect frame count for animated PNGs
2 parents b6c4bb8 + 2a13a71 commit 703d1ba

File tree

4 files changed

+125
-6
lines changed

4 files changed

+125
-6
lines changed

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
214214
break;
215215
case PngChunkType.FrameData:
216216
{
217-
if (frameCount >= this.maxFrames)
217+
if (frameCount > this.maxFrames)
218218
{
219219
goto EOF;
220220
}
@@ -275,7 +275,7 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
275275
previousFrameControl = currentFrameControl;
276276
}
277277

278-
if (frameCount >= this.maxFrames)
278+
if (frameCount > this.maxFrames)
279279
{
280280
goto EOF;
281281
}
@@ -402,7 +402,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
402402
break;
403403
case PngChunkType.FrameControl:
404404
++frameCount;
405-
if (frameCount >= this.maxFrames)
405+
if (frameCount > this.maxFrames)
406406
{
407407
break;
408408
}
@@ -411,8 +411,12 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
411411

412412
break;
413413
case PngChunkType.FrameData:
414-
if (frameCount >= this.maxFrames)
414+
if (frameCount > this.maxFrames)
415415
{
416+
// Must skip the chunk data even when we've hit maxFrames, because TryReadChunk
417+
// restores the stream position to the start of the fdAT data after CRC validation.
418+
this.SkipChunkDataAndCrc(chunk);
419+
this.SkipRemainingFrameDataChunks(buffer);
416420
break;
417421
}
418422

@@ -428,9 +432,10 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
428432

429433
InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
430434

431-
// Skip sequence number
432-
this.currentStream.Skip(4);
435+
// Skip data for this and all remaining FrameData chunks belonging to the same frame
436+
// (comparable to how Decode consumes them via ReadScanlines + ReadNextFrameDataChunk).
433437
this.SkipChunkDataAndCrc(chunk);
438+
this.SkipRemainingFrameDataChunks(buffer);
434439
break;
435440
case PngChunkType.Data:
436441

@@ -2093,6 +2098,31 @@ private int ReadNextFrameDataChunk()
20932098
return 0;
20942099
}
20952100

2101+
/// <summary>
2102+
/// Skips any remaining <see cref="PngChunkType.FrameData"/> chunks belonging to the current frame.
2103+
/// This mirrors how <see cref="ReadNextFrameDataChunk"/> is used during decoding:
2104+
/// consecutive fdAT chunks are consumed until a non-fdAT chunk is encountered,
2105+
/// which is stored in <see cref="nextChunk"/> for the next iteration.
2106+
/// </summary>
2107+
/// <param name="buffer">Temporary buffer.</param>
2108+
private void SkipRemainingFrameDataChunks(Span<byte> buffer)
2109+
{
2110+
while (this.TryReadChunk(buffer, out PngChunk chunk))
2111+
{
2112+
if (chunk.Type is PngChunkType.FrameData)
2113+
{
2114+
chunk.Data?.Dispose();
2115+
this.SkipChunkDataAndCrc(chunk);
2116+
}
2117+
else
2118+
{
2119+
// Not a FrameData chunk; store it so the next TryReadChunk call returns it.
2120+
this.nextChunk = chunk;
2121+
return;
2122+
}
2123+
}
2124+
}
2125+
20962126
/// <summary>
20972127
/// Reads a chunk from the stream.
20982128
/// </summary>

tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,91 @@ public void Identify_IgnoreCrcErrors(string imagePath, int expectedPixelSize)
411411
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
412412
}
413413

414+
[Fact]
415+
public void Identify_AnimatedPng_ReadsFrameCountCorrectly()
416+
{
417+
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
418+
419+
using MemoryStream stream = new(testFile.Bytes, false);
420+
ImageInfo imageInfo = Image.Identify(stream);
421+
422+
Assert.NotNull(imageInfo);
423+
Assert.Equal(48, imageInfo.FrameMetadataCollection.Count);
424+
}
425+
426+
[Fact]
427+
public void Identify_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly()
428+
{
429+
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
430+
431+
using MemoryStream stream = new(testFile.Bytes, false);
432+
ImageInfo imageInfo = Image.Identify(new DecoderOptions { MaxFrames = 40 }, stream);
433+
434+
Assert.NotNull(imageInfo);
435+
Assert.Equal(40, imageInfo.FrameMetadataCollection.Count);
436+
}
437+
438+
[Fact]
439+
public void Load_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly()
440+
{
441+
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
442+
443+
using MemoryStream stream = new(testFile.Bytes, false);
444+
using Image image = Image.Load(new DecoderOptions { MaxFrames = 40 }, stream);
445+
446+
Assert.NotNull(image);
447+
Assert.Equal(40, image.Frames.Count);
448+
}
449+
450+
[Theory]
451+
[InlineData(1)]
452+
[InlineData(2)]
453+
[InlineData(5)]
454+
[InlineData(10)]
455+
[InlineData(100)]
456+
public void Identify_AnimatedPng_FrameCount_MatchesDecode(int frameCount)
457+
{
458+
using Image<Rgba32> image = new(10, 10, Color.Red.ToPixel<Rgba32>());
459+
for (int i = 1; i < frameCount; i++)
460+
{
461+
using ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
462+
image.Frames.AddFrame(frame);
463+
}
464+
465+
using MemoryStream stream = new();
466+
image.Save(stream, new PngEncoder());
467+
stream.Position = 0;
468+
469+
ImageInfo imageInfo = Image.Identify(stream);
470+
471+
Assert.NotNull(imageInfo);
472+
Assert.Equal(frameCount, imageInfo.FrameMetadataCollection.Count);
473+
}
474+
475+
[Theory]
476+
[InlineData(1)]
477+
[InlineData(2)]
478+
[InlineData(5)]
479+
[InlineData(10)]
480+
[InlineData(100)]
481+
public void Decode_AnimatedPng_FrameCount(int frameCount)
482+
{
483+
using Image<Rgba32> image = new(10, 10, Color.Red.ToPixel<Rgba32>());
484+
for (int i = 1; i < frameCount; i++)
485+
{
486+
using ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
487+
image.Frames.AddFrame(frame);
488+
}
489+
490+
using MemoryStream stream = new();
491+
image.Save(stream, new PngEncoder());
492+
stream.Position = 0;
493+
494+
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
495+
496+
Assert.Equal(frameCount, decoded.Frames.Count);
497+
}
498+
414499
[Theory]
415500
[WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)]
416501
public void Decode_MissingDataChunk_ThrowsException<TPixel>(TestImageProvider<TPixel> provider)

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public static class Png
7676
public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png";
7777
public const string FrameOffset = "Png/animated/frame-offset.png";
7878
public const string DefaultNotAnimated = "Png/animated/default-not-animated.png";
79+
public const string AnimatedFrameCount = "Png/animated/issue-animated-frame-count.png";
7980
public const string Issue2666 = "Png/issues/Issue_2666.png";
8081
public const string Issue2882 = "Png/issues/Issue_2882.png";
8182

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)