From 417667a12c5ba1871b3b33d623e2aaa21020e1d9 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 12:33:23 -1000 Subject: [PATCH 01/13] Add test --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 3 ++- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png | 3 +++ .../Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png | 3 +++ .../Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png | 3 +++ .../Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png | 3 +++ .../Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png | 3 +++ tests/Images/Input/Png/animated/frame-offset.png | 3 +++ 8 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png create mode 100644 tests/Images/Input/Png/animated/frame-offset.png diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 9b165526eb..336b89376f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -87,7 +87,8 @@ public partial class PngDecoderTests TestImages.Png.DisposeBackgroundRegion, TestImages.Png.DisposePreviousFirst, TestImages.Png.DisposeBackgroundBeforeRegion, - TestImages.Png.BlendOverMultiple + TestImages.Png.BlendOverMultiple, + TestImages.Png.FrameOffset }; [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fe633fddc3..7e6ac9e8d8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -73,6 +73,7 @@ public static class Png public const string DisposeBackgroundRegion = "Png/animated/15-dispose-background-region.png"; public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png"; public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png"; + public const string FrameOffset = "Png/animated/frame-offset.png"; public const string Issue2666 = "Png/issues/Issue_2666.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png new file mode 100644 index 0000000000..b9fa24c930 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84e2353264e3488122f4d488d7c4b198ff5192ad0c662c7fb0a369c957ecc7ea +size 353 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png new file mode 100644 index 0000000000..6f3a27187c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2916711d3f4d72eb66a5cfc2b40a3318eb4cce5b367658cfc7e3b573fd39cc33 +size 693 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png new file mode 100644 index 0000000000..50911cce57 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9d5503414ccefa6b66661b1e93c2c3f6e4491f14af006a71153cecf43b52f5 +size 806 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png new file mode 100644 index 0000000000..89d2f95706 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe42b7dc6524d5589ad680650f4bcd181319b40b258b31e0932d6e936818e980 +size 570 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png new file mode 100644 index 0000000000..c3f2b99b8b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8002ff5b3451b348f285eec15dd7a093c62d11d8b77c3ead9ac89ca6eb29977d +size 669 diff --git a/tests/Images/Input/Png/animated/frame-offset.png b/tests/Images/Input/Png/animated/frame-offset.png new file mode 100644 index 0000000000..4eebb44a3d --- /dev/null +++ b/tests/Images/Input/Png/animated/frame-offset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c019073841b48b02cb07c779fed8654c6052aee700e7620d07f5d775d97088f +size 2156 From d933db75ed5fff7e3ada9ded4d5562b2982ca260 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 13:50:40 -1000 Subject: [PATCH 02/13] Fix ProcessInterlacedPaletteScanline not obeying frameControl.XOffset --- src/ImageSharp/Formats/Png/PngScanlineProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index f217515e3c..b327ed21ba 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -198,8 +198,9 @@ public static void ProcessInterlacedPaletteScanline( ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); + uint offset = pixelOffset + frameControl.XOffset; - for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) { uint index = Unsafe.Add(ref scanlineSpanRef, o); pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); From 63b7ecd87e1d6659874123ac7c8e5baefdc3dc53 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 17:49:33 -1000 Subject: [PATCH 03/13] Fix frame dispose operation handling --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 25 +++++++++++-------- .../00.png | 4 +-- .../01.png | 4 +-- .../02.png | 4 +-- .../03.png | 4 +-- .../04.png | 4 +-- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a96c53c104..caa104c6e9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -240,8 +240,13 @@ public Image Decode(BufferedReadStream stream, CancellationToken currentFrameControl.Value, cancellationToken); - previousFrame = currentFrame; - previousFrameControl = currentFrameControl; + // if current frame dispose is restore to previous, then from future frame's perspective, it never happened + if (currentFrameControl.Value.DisposeOperation != PngDisposalMethod.RestoreToPrevious) + { + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; + } + break; case PngChunkType.Data: @@ -639,18 +644,18 @@ private void InitializeFrame( out ImageFrame frame) where TPixel : unmanaged, IPixel { - // We create a clone of the previous frame and add it. - // We will overpaint the difference of pixels on the current frame to create a complete image. - // This ensures that we have enough pixel data to process without distortion. #2450 frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); - // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. - if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground - || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious)) + // if restoring to before first frame, restore to background + if (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious) + { + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(); + pixelRegion.Clear(); + } + else if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground) { Rectangle restoreArea = previousFrameControl.Bounds; - Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea); - Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(restoreArea); pixelRegion.Clear(); } diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png index b9fa24c930..870ed61a44 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84e2353264e3488122f4d488d7c4b198ff5192ad0c662c7fb0a369c957ecc7ea -size 353 +oid sha256:b85aaf7153e0ca538856a58d7b069bcc13fadc468ea603c85f8782cc691f86c3 +size 387 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png index 6f3a27187c..cab85d9466 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2916711d3f4d72eb66a5cfc2b40a3318eb4cce5b367658cfc7e3b573fd39cc33 -size 693 +oid sha256:fcb83d6893dcfd869b764ff9846c259eaa0caf26cec3f0fc2cbae2c26f2eeaa5 +size 660 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png index 50911cce57..1a2c5adcf0 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f9d5503414ccefa6b66661b1e93c2c3f6e4491f14af006a71153cecf43b52f5 -size 806 +oid sha256:562ec382f6d2af68e66092bf6949f66147d5f608d3c618eea5a7c1ea400737ff +size 768 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png index 89d2f95706..d850459ee8 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe42b7dc6524d5589ad680650f4bcd181319b40b258b31e0932d6e936818e980 -size 570 +oid sha256:d12a7791b960072e32b78bd9aaf456dc99341eea1c66ea05050433d8c082c6ac +size 579 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png index c3f2b99b8b..000b0567de 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8002ff5b3451b348f285eec15dd7a093c62d11d8b77c3ead9ac89ca6eb29977d -size 669 +oid sha256:2db38d7ffcc95c23a5c94a06f10c6cc67406ae581a955c99ede4af97b1a044f8 +size 628 From d15a2e79a4fbfbaa150e436e6afa938da131f05d Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 18:14:23 -1000 Subject: [PATCH 04/13] Add test for default image not animated --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 3 ++- tests/ImageSharp.Tests/TestImages.cs | 1 + .../00.png | 3 +++ .../01.png | 3 +++ tests/Images/Input/Png/animated/default-not-animated.png | 3 +++ 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/01.png create mode 100644 tests/Images/Input/Png/animated/default-not-animated.png diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 336b89376f..9ff368d4ec 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -88,7 +88,8 @@ public partial class PngDecoderTests TestImages.Png.DisposePreviousFirst, TestImages.Png.DisposeBackgroundBeforeRegion, TestImages.Png.BlendOverMultiple, - TestImages.Png.FrameOffset + TestImages.Png.FrameOffset, + TestImages.Png.DefaultNotAnimated }; [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 7e6ac9e8d8..afb13363d4 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -74,6 +74,7 @@ public static class Png public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png"; public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png"; public const string FrameOffset = "Png/animated/frame-offset.png"; + public const string DefaultNotAnimated = "Png/animated/default-not-animated.png"; public const string Issue2666 = "Png/issues/Issue_2666.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/00.png new file mode 100644 index 0000000000..4c5ea8169a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d4716e18655be53630d6d50daebe8c38e0eedb2432c7a73840b55d1473d5944 +size 1050 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/01.png new file mode 100644 index 0000000000..790fe45e4c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b5a6d3cf1a777f6b719c2a1cf79bffe2251355d75e6c0f7ce7a973b3d033419 +size 1177 diff --git a/tests/Images/Input/Png/animated/default-not-animated.png b/tests/Images/Input/Png/animated/default-not-animated.png new file mode 100644 index 0000000000..1ed72698d5 --- /dev/null +++ b/tests/Images/Input/Png/animated/default-not-animated.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:647d484c8f320b55824b9219270524df3edc434a4793e1627e0ee14af8d6e4f8 +size 1689 From 9b8ef108d41877464418a16451aac87eec37c8bb Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 18:28:03 -1000 Subject: [PATCH 05/13] Fix handling of case where default image isn't animated --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 22 +++++++++++--------- src/ImageSharp/Formats/Png/PngMetadata.cs | 5 +++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index caa104c6e9..3bc2ccf492 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -228,8 +228,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken PngThrowHelper.ThrowMissingFrameControl(); } - previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); - this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame); + this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); this.currentStream.Position += 4; this.ReadScanlines( @@ -249,7 +248,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken break; case PngChunkType.Data: - + pngMetadata.DefaultImageAnimated = currentFrameControl != null; currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); if (image is null) { @@ -266,9 +265,12 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.ReadNextDataChunk, currentFrameControl.Value, cancellationToken); + if (pngMetadata.DefaultImageAnimated) + { + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; + } - previousFrame = currentFrame; - previousFrameControl = currentFrameControl; break; case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); @@ -637,7 +639,7 @@ private void InitializeImage(ImageMetadata metadata, FrameControl frameC /// The previous frame. /// The created frame private void InitializeFrame( - FrameControl previousFrameControl, + FrameControl? previousFrameControl, FrameControl currentFrameControl, Image image, ImageFrame? previousFrame, @@ -646,15 +648,15 @@ private void InitializeFrame( { frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); - // if restoring to before first frame, restore to background - if (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious) + // If restoring to before first frame, restore to background. Same if first frame (previousFrameControl null). + if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious)) { Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(); pixelRegion.Clear(); } - else if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground) + else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground) { - Rectangle restoreArea = previousFrameControl.Bounds; + Rectangle restoreArea = previousFrameControl.Value.Bounds; Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(restoreArea); pixelRegion.Clear(); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 93ddcf2636..c4ff3bbe24 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -83,6 +83,11 @@ private PngMetadata(PngMetadata other) /// public uint RepeatCount { get; set; } = 1; + /// + /// Gets or sets a value indicating whether the default image is shown as part of the animated sequence + /// + public bool DefaultImageAnimated { get; set; } + /// public IDeepCloneable DeepClone() => new PngMetadata(this); From 85fd4311f1caa30bb37c990842f977ed9a1dec81 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 19:23:35 -1000 Subject: [PATCH 06/13] Fix PngMetadata copy --- src/ImageSharp/Formats/Png/PngMetadata.cs | 3 ++- tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index c4ff3bbe24..766377f7c9 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -29,6 +29,7 @@ private PngMetadata(PngMetadata other) this.InterlaceMethod = other.InterlaceMethod; this.TransparentColor = other.TransparentColor; this.RepeatCount = other.RepeatCount; + this.DefaultImageAnimated = other.DefaultImageAnimated; if (other.ColorTable?.Length > 0) { @@ -86,7 +87,7 @@ private PngMetadata(PngMetadata other) /// /// Gets or sets a value indicating whether the default image is shown as part of the animated sequence /// - public bool DefaultImageAnimated { get; set; } + public bool DefaultImageAnimated { get; set; } = true; /// public IDeepCloneable DeepClone() => new PngMetadata(this); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index b3c122a7a8..4f9ba9abee 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -32,7 +32,8 @@ public void CloneIsDeep() InterlaceMethod = PngInterlaceMode.Adam7, Gamma = 2, TextData = new List { new PngTextData("name", "value", "foo", "bar") }, - RepeatCount = 123 + RepeatCount = 123, + DefaultImageAnimated = false }; PngMetadata clone = (PngMetadata)meta.DeepClone(); @@ -44,6 +45,7 @@ public void CloneIsDeep() Assert.False(meta.TextData.Equals(clone.TextData)); Assert.True(meta.TextData.SequenceEqual(clone.TextData)); Assert.True(meta.RepeatCount == clone.RepeatCount); + Assert.True(meta.DefaultImageAnimated == clone.DefaultImageAnimated); clone.BitDepth = PngBitDepth.Bit2; clone.ColorType = PngColorType.Palette; From addec72467bacd9fc2152549f9ce22e1b6074006 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Thu, 28 Mar 2024 20:11:43 -1000 Subject: [PATCH 07/13] Make PngEncoder respect DefaultImageAnimated --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 43 +++++++++++++------ .../Formats/Png/PngEncoderTests.cs | 23 ++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ea94270f2f..d1a600737b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -161,6 +161,7 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame? clonedFrame = null; ImageFrame currentFrame = image.Frames.RootFrame; + int currentFrameIndex = 0; bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; if (clearTransparency) @@ -190,28 +191,49 @@ public void Encode(Image image, Stream stream, CancellationToken if (image.Frames.Count > 1) { this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount); + } + + // If the first frame isn't animated, write it as usual and skip it when writing animated frames + if (!pngMetadata.DefaultImageAnimated || image.Frames.Count == 1) + { + FrameControl frameControl = new((uint)this.width, (uint)this.height); + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + currentFrameIndex++; + } - // Write the first frame. + if (image.Frames.Count > 1) + { + // Write the first animated frame. + currentFrame = image.Frames[currentFrameIndex]; PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + uint sequenceNumber = 1; + if (pngMetadata.DefaultImageAnimated) + { + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + } + else + { + sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true); + } + + currentFrameIndex++; // Capture the global palette for reuse on subsequent frames. ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); // Write following frames. - uint increment = 0; ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); - for (int i = 1; i < image.Frames.Count; i++) + for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; - currentFrame = image.Frames[i]; - ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + currentFrame = image.Frames[currentFrameIndex]; + ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; frameMetadata = GetPngFrameMetadata(currentFrame); bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; @@ -232,22 +254,17 @@ public void Encode(Image image, Stream stream, CancellationToken } // Each frame control sequence number must be incremented by the number of frame data chunks that follow. - frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, (uint)i + increment); + frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); // Dispose of previous quantized frame and reassign. quantized?.Dispose(); quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); - increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true); + sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; previousFrame = currentFrame; previousDisposal = frameMetadata.DisposalMethod; } } - else - { - FrameControl frameControl = new((uint)this.width, (uint)this.height); - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); - } this.WriteEndChunk(stream); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 950f1d2e3a..bbd7ff4e49 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -586,6 +586,29 @@ public void Encode_AnimatedFormatTransform_FromWebp(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + using MemoryStream memStream = new(); + image.Save(memStream, PngEncoder); + memStream.Position = 0; + + image.DebugSave(provider: provider, encoder: PngEncoder, null, false); + + using Image output = Image.Load(memStream); + ImageComparer.Exact.VerifySimilarity(output, image); + + Assert.Equal(2, image.Frames.Count); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + PngMetadata originalMetadata = image.Metadata.GetPngMetadata(); + PngMetadata outputMetadata = output.Metadata.GetPngMetadata(); + Assert.Equal(originalMetadata.DefaultImageAnimated, outputMetadata.DefaultImageAnimated); + } + [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) From 9ae8be4c8afc28f6c648f913b558ba23e6a8e191 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Fri, 29 Mar 2024 21:34:48 -1000 Subject: [PATCH 08/13] Add more tests --- .../Formats/Png/PngEncoderTests.Chunks.cs | 33 +++++++++++++++++++ .../Formats/Png/PngMetadataTests.cs | 20 +++++++++++ 2 files changed, 53 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 044da21938..2e3390298b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; // ReSharper disable InconsistentNaming @@ -59,6 +60,38 @@ public void EndChunk_IsLast() } } + [Theory] + [WithFile(TestImages.Png.DefaultNotAnimated, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void AcTL_CorrectlyWritten(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + PngMetadata metadata = image.Metadata.GetPngMetadata(); + int correctFrameCount = image.Frames.Count - (metadata.DefaultImageAnimated ? 0 : 1); + using MemoryStream memStream = new(); + image.Save(memStream, PngEncoder); + memStream.Position = 0; + Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header. + bool foundAcTl = false; + while (bytesSpan.Length > 0 && !foundAcTl) + { + int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan[..4]); + PngChunkType type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4)); + if (type == PngChunkType.AnimationControl) + { + AnimationControl control = AnimationControl.Parse(bytesSpan[8..]); + foundAcTl = true; + Assert.True(control.NumberFrames == correctFrameCount); + Assert.True(control.NumberPlays == metadata.RepeatCount); + } + + bytesSpan = bytesSpan[(4 + 4 + length + 4)..]; + } + + Assert.True(foundAcTl); + } + [Theory] [InlineData(PngChunkType.Gamma)] [InlineData(PngChunkType.Chroma)] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index 4f9ba9abee..8308935d04 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -146,6 +146,26 @@ public void Decode_ReadsExifData(TestImageProvider provider) VerifyExifDataIsPresent(exif); } + [Theory] + [WithFile(TestImages.Png.DefaultNotAnimated, PixelTypes.Rgba32)] + public void Decode_IdentifiesDefaultFrameNotAnimated(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + Assert.False(meta.DefaultImageAnimated); + } + + [Theory] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void Decode_IdentifiesDefaultFrameAnimated(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + Assert.True(meta.DefaultImageAnimated); + } + [Theory] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider) From ceb7dc0e7e26ee289cb8e2743ceb5b6793f65c41 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Fri, 29 Mar 2024 21:35:53 -1000 Subject: [PATCH 09/13] Fix incorrect acTL frame count --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index d1a600737b..e0f7ed9489 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -190,7 +190,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount); + this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.DefaultImageAnimated ? 0 : 1)), pngMetadata.RepeatCount); } // If the first frame isn't animated, write it as usual and skip it when writing animated frames From 4698e8ca3b4a6a453a45cace6554f40148c53d3e Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Mon, 1 Apr 2024 17:03:25 -0700 Subject: [PATCH 10/13] re-add comment --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3bc2ccf492..dd2ce9eb14 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -648,7 +648,8 @@ private void InitializeFrame( { frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); - // If restoring to before first frame, restore to background. Same if first frame (previousFrameControl null). + // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + // So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null). if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious)) { Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(); From b1a5593b20e89786343d5d7d71a2c08dd606b1e9 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Mon, 1 Apr 2024 17:28:27 -0700 Subject: [PATCH 11/13] Add test for case with frame offsets --- .../Formats/Png/PngEncoderTests.cs | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index bbd7ff4e49..6c4047be8e 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -447,6 +447,8 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color [Theory] [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.DefaultNotAnimated, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.FrameOffset, PixelTypes.Rgba32)] public void Encode_APng(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -458,15 +460,17 @@ public void Encode_APng(TestImageProvider provider) image.DebugSave(provider: provider, encoder: PngEncoder, null, false); using Image output = Image.Load(memStream); - ImageComparer.Exact.VerifySimilarity(output, image); - Assert.Equal(5, image.Frames.Count); + // some loss from original, due to compositing + ImageComparer.TolerantPercentage(0.01f).VerifySimilarity(output, image); + Assert.Equal(image.Frames.Count, output.Frames.Count); PngMetadata originalMetadata = image.Metadata.GetPngMetadata(); PngMetadata outputMetadata = output.Metadata.GetPngMetadata(); Assert.Equal(originalMetadata.RepeatCount, outputMetadata.RepeatCount); + Assert.Equal(originalMetadata.DefaultImageAnimated, outputMetadata.DefaultImageAnimated); for (int i = 0; i < image.Frames.Count; i++) { @@ -586,29 +590,6 @@ public void Encode_AnimatedFormatTransform_FromWebp(TestImageProvider(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(PngDecoder.Instance); - using MemoryStream memStream = new(); - image.Save(memStream, PngEncoder); - memStream.Position = 0; - - image.DebugSave(provider: provider, encoder: PngEncoder, null, false); - - using Image output = Image.Load(memStream); - ImageComparer.Exact.VerifySimilarity(output, image); - - Assert.Equal(2, image.Frames.Count); - Assert.Equal(image.Frames.Count, output.Frames.Count); - - PngMetadata originalMetadata = image.Metadata.GetPngMetadata(); - PngMetadata outputMetadata = output.Metadata.GetPngMetadata(); - Assert.Equal(originalMetadata.DefaultImageAnimated, outputMetadata.DefaultImageAnimated); - } - [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) From 848013921281a7381ea4328f4f3d37f3587b01bf Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Mon, 1 Apr 2024 17:56:21 -0700 Subject: [PATCH 12/13] re-add rest of comment --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index dd2ce9eb14..1afb65566d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -646,6 +646,9 @@ private void InitializeFrame( out ImageFrame frame) where TPixel : unmanaged, IPixel { + // We create a clone of the previous frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. From c8eab788dd8d496703ba7c82bd595fe2fea81d83 Mon Sep 17 00:00:00 2001 From: SpaceCheetah Date: Tue, 2 Apr 2024 23:00:15 -0700 Subject: [PATCH 13/13] Rename DefaultImageAnimated to AnimateRootFrame --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 4 ++-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 6 +++--- src/ImageSharp/Formats/Png/PngMetadata.cs | 6 +++--- .../Formats/Png/PngEncoderTests.Chunks.cs | 2 +- tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs | 2 +- tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 1afb65566d..c4b10b4dc9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -248,7 +248,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken break; case PngChunkType.Data: - pngMetadata.DefaultImageAnimated = currentFrameControl != null; + pngMetadata.AnimateRootFrame = currentFrameControl != null; currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); if (image is null) { @@ -265,7 +265,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.ReadNextDataChunk, currentFrameControl.Value, cancellationToken); - if (pngMetadata.DefaultImageAnimated) + if (pngMetadata.AnimateRootFrame) { previousFrame = currentFrame; previousFrameControl = currentFrameControl; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e0f7ed9489..802e4dd6a4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -190,11 +190,11 @@ public void Encode(Image image, Stream stream, CancellationToken if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.DefaultImageAnimated ? 0 : 1)), pngMetadata.RepeatCount); + this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount); } // If the first frame isn't animated, write it as usual and skip it when writing animated frames - if (!pngMetadata.DefaultImageAnimated || image.Frames.Count == 1) + if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1) { FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); @@ -209,7 +209,7 @@ public void Encode(Image image, Stream stream, CancellationToken PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); uint sequenceNumber = 1; - if (pngMetadata.DefaultImageAnimated) + if (pngMetadata.AnimateRootFrame) { this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 766377f7c9..d9028dd807 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -29,7 +29,7 @@ private PngMetadata(PngMetadata other) this.InterlaceMethod = other.InterlaceMethod; this.TransparentColor = other.TransparentColor; this.RepeatCount = other.RepeatCount; - this.DefaultImageAnimated = other.DefaultImageAnimated; + this.AnimateRootFrame = other.AnimateRootFrame; if (other.ColorTable?.Length > 0) { @@ -85,9 +85,9 @@ private PngMetadata(PngMetadata other) public uint RepeatCount { get; set; } = 1; /// - /// Gets or sets a value indicating whether the default image is shown as part of the animated sequence + /// Gets or sets a value indicating whether the root frame is shown as part of the animated sequence /// - public bool DefaultImageAnimated { get; set; } = true; + public bool AnimateRootFrame { get; set; } = true; /// public IDeepCloneable DeepClone() => new PngMetadata(this); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 2e3390298b..76fd260dd5 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -68,7 +68,7 @@ public void AcTL_CorrectlyWritten(TestImageProvider provider) { using Image image = provider.GetImage(PngDecoder.Instance); PngMetadata metadata = image.Metadata.GetPngMetadata(); - int correctFrameCount = image.Frames.Count - (metadata.DefaultImageAnimated ? 0 : 1); + int correctFrameCount = image.Frames.Count - (metadata.AnimateRootFrame ? 0 : 1); using MemoryStream memStream = new(); image.Save(memStream, PngEncoder); memStream.Position = 0; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 6c4047be8e..ca5aae961c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -470,7 +470,7 @@ public void Encode_APng(TestImageProvider provider) PngMetadata outputMetadata = output.Metadata.GetPngMetadata(); Assert.Equal(originalMetadata.RepeatCount, outputMetadata.RepeatCount); - Assert.Equal(originalMetadata.DefaultImageAnimated, outputMetadata.DefaultImageAnimated); + Assert.Equal(originalMetadata.AnimateRootFrame, outputMetadata.AnimateRootFrame); for (int i = 0; i < image.Frames.Count; i++) { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index 8308935d04..225e4deef2 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -33,7 +33,7 @@ public void CloneIsDeep() Gamma = 2, TextData = new List { new PngTextData("name", "value", "foo", "bar") }, RepeatCount = 123, - DefaultImageAnimated = false + AnimateRootFrame = false }; PngMetadata clone = (PngMetadata)meta.DeepClone(); @@ -45,7 +45,7 @@ public void CloneIsDeep() Assert.False(meta.TextData.Equals(clone.TextData)); Assert.True(meta.TextData.SequenceEqual(clone.TextData)); Assert.True(meta.RepeatCount == clone.RepeatCount); - Assert.True(meta.DefaultImageAnimated == clone.DefaultImageAnimated); + Assert.True(meta.AnimateRootFrame == clone.AnimateRootFrame); clone.BitDepth = PngBitDepth.Bit2; clone.ColorType = PngColorType.Palette; @@ -153,7 +153,7 @@ public void Decode_IdentifiesDefaultFrameNotAnimated(TestImageProvider image = provider.GetImage(PngDecoder.Instance); PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); - Assert.False(meta.DefaultImageAnimated); + Assert.False(meta.AnimateRootFrame); } [Theory] @@ -163,7 +163,7 @@ public void Decode_IdentifiesDefaultFrameAnimated(TestImageProvider image = provider.GetImage(PngDecoder.Instance); PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); - Assert.True(meta.DefaultImageAnimated); + Assert.True(meta.AnimateRootFrame); } [Theory]