diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs
index 8f06e5886..667a47fc9 100644
--- a/src/ImageSharp/Common/Helpers/RiffHelper.cs
+++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs
@@ -3,6 +3,7 @@
using System.Buffers.Binary;
using System.Text;
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
namespace SixLabors.ImageSharp.Common.Helpers;
@@ -107,6 +108,7 @@ internal static class RiffHelper
position++;
}
+ // Add the size of the encoded file to the Riff header.
BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
stream.Position = sizePosition;
stream.Write(buffer);
@@ -120,5 +122,18 @@ internal static class RiffHelper
return sizePosition;
}
- public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition);
+ public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition)
+ {
+ EndWriteChunk(stream, sizePosition + 4);
+
+ // Write the VP8X chunk if necessary.
+ if (updateVp8x)
+ {
+ long position = stream.Position;
+
+ stream.Position = sizePosition + 12;
+ vp8x.WriteTo(stream);
+ stream.Position = position;
+ }
+ }
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index abd5cadeb..dd5e16d7b 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -228,8 +228,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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(
@@ -240,11 +239,16 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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:
-
+ pngMetadata.AnimateRootFrame = currentFrameControl != null;
currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
{
@@ -261,9 +265,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.ReadNextDataChunk,
currentFrameControl.Value,
cancellationToken);
+ if (pngMetadata.AnimateRootFrame)
+ {
+ previousFrame = currentFrame;
+ previousFrameControl = currentFrameControl;
+ }
- previousFrame = currentFrame;
- previousFrameControl = currentFrameControl;
break;
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
@@ -632,7 +639,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// The previous frame.
/// The created frame
private void InitializeFrame(
- FrameControl previousFrameControl,
+ FrameControl? previousFrameControl,
FrameControl currentFrameControl,
Image image,
ImageFrame? previousFrame,
@@ -645,12 +652,16 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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))
+ // 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();
+ pixelRegion.Clear();
+ }
+ else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground)
{
- Rectangle restoreArea = previousFrameControl.Bounds;
- Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
- Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
+ Rectangle restoreArea = previousFrameControl.Value.Bounds;
+ Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
pixelRegion.Clear();
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index ea94270f2..802e4dd6a 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -161,6 +161,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ImageFrame? clonedFrame = null;
ImageFrame currentFrame = image.Frames.RootFrame;
+ int currentFrameIndex = 0;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
@@ -189,29 +190,50 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (image.Frames.Count > 1)
{
- this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, 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.AnimateRootFrame || 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.AnimateRootFrame)
+ {
+ 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}
// 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/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 93ddcf263..d9028dd80 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -29,6 +29,7 @@ public class PngMetadata : IDeepCloneable
this.InterlaceMethod = other.InterlaceMethod;
this.TransparentColor = other.TransparentColor;
this.RepeatCount = other.RepeatCount;
+ this.AnimateRootFrame = other.AnimateRootFrame;
if (other.ColorTable?.Length > 0)
{
@@ -83,6 +84,11 @@ public class PngMetadata : IDeepCloneable
///
public uint RepeatCount { get; set; } = 1;
+ ///
+ /// Gets or sets a value indicating whether the root frame is shown as part of the animated sequence
+ ///
+ public bool AnimateRootFrame { get; set; } = true;
+
///
public IDeepCloneable DeepClone() => new PngMetadata(this);
diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
index f217515e3..b327ed21b 100644
--- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
+++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
@@ -198,8 +198,9 @@ internal static class PngScanlineProcessor
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());
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index 9ffda0f51..651e68011 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -88,7 +88,8 @@ internal abstract class BitWriterBase
/// The color profile.
/// Flag indicating, if a alpha channel is present.
/// Flag indicating, if an animation parameter is present.
- public static void WriteTrunksBeforeData(
+ /// A or a default instance.
+ public static WebpVp8X WriteTrunksBeforeData(
Stream stream,
uint width,
uint height,
@@ -102,16 +103,19 @@ internal abstract class BitWriterBase
RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
// Write VP8X, header if necessary.
+ WebpVp8X vp8x = default;
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
if (isVp8X)
{
- WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
+ vp8x = WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
if (iccProfile != null)
{
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
}
}
+
+ return vp8x;
}
///
@@ -124,10 +128,16 @@ internal abstract class BitWriterBase
/// Write the trunks after data trunk.
///
/// The stream to write to.
- /// The exif profile.
+ /// The VP8X chunk.
+ /// Whether to update the chunk.
+ /// The initial position of the stream before encoding.
+ /// The EXIF profile.
/// The XMP profile.
public static void WriteTrunksAfterData(
Stream stream,
+ in WebpVp8X vp8x,
+ bool updateVp8x,
+ long initialPosition,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{
@@ -141,7 +151,7 @@ internal abstract class BitWriterBase
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}
- RiffHelper.EndWriteRiffFile(stream, 4);
+ RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition);
}
///
@@ -186,19 +196,21 @@ internal abstract class BitWriterBase
/// Writes a VP8X header to the stream.
///
/// The stream to write to.
- /// A exif profile or null, if it does not exist.
- /// A XMP profile or null, if it does not exist.
+ /// An EXIF profile or null, if it does not exist.
+ /// An XMP profile or null, if it does not exist.
/// The color profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
/// Flag indicating, if an animation parameter is present.
- protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation)
+ protected static WebpVp8X WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation)
{
WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
chunk.Validate(MaxDimension, MaxCanvasPixels);
chunk.WriteTo(stream);
+
+ return chunk;
}
}
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
index 70d6870ce..fc88f8faa 100644
--- a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
@@ -5,7 +5,7 @@ using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
-internal readonly struct WebpVp8X
+internal readonly struct WebpVp8X : IEquatable
{
public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
{
@@ -53,6 +53,24 @@ internal readonly struct WebpVp8X
///
public uint Height { get; }
+ public static bool operator ==(WebpVp8X left, WebpVp8X right) => left.Equals(right);
+
+ public static bool operator !=(WebpVp8X left, WebpVp8X right) => !(left == right);
+
+ public override bool Equals(object? obj) => obj is WebpVp8X x && this.Equals(x);
+
+ public bool Equals(WebpVp8X other)
+ => this.HasAnimation == other.HasAnimation
+ && this.HasXmp == other.HasXmp
+ && this.HasExif == other.HasExif
+ && this.HasAlpha == other.HasAlpha
+ && this.HasIcc == other.HasIcc
+ && this.Width == other.Width
+ && this.Height == other.Height;
+
+ public override int GetHashCode()
+ => HashCode.Combine(this.HasAnimation, this.HasXmp, this.HasExif, this.HasAlpha, this.HasIcc, this.Width, this.Height);
+
public void Validate(uint maxDimension, ulong maxCanvasPixels)
{
if (this.Width > maxDimension || this.Height > maxDimension)
@@ -67,6 +85,9 @@ internal readonly struct WebpVp8X
}
}
+ public WebpVp8X WithAlpha(bool hasAlpha)
+ => new(this.HasAnimation, this.HasXmp, this.HasExif, hasAlpha, this.HasIcc, this.Width, this.Height);
+
public void WriteTo(Stream stream)
{
byte flags = 0;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 518c09ff4..485225ab8 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -237,7 +237,7 @@ internal class Vp8LEncoder : IDisposable
///
public Vp8LHashChain HashChain { get; }
- public void EncodeHeader(Image image, Stream stream, bool hasAnimation)
+ public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
// Write bytes from the bit-writer buffer to the stream.
@@ -247,7 +247,8 @@ internal class Vp8LEncoder : IDisposable
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
- BitWriterBase.WriteTrunksBeforeData(
+ // The alpha flag is updated following encoding.
+ WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)image.Width,
(uint)image.Height,
@@ -262,9 +263,11 @@ internal class Vp8LEncoder : IDisposable
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
+
+ return vp8x;
}
- public void EncodeFooter(Image image, Stream stream)
+ public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel
{
// Write bytes from the bit-writer buffer to the stream.
@@ -273,7 +276,9 @@ internal class Vp8LEncoder : IDisposable
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
- BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ bool updateVp8x = hasAlpha && vp8x != default;
+ WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x;
+ BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile);
}
///
@@ -285,7 +290,8 @@ internal class Vp8LEncoder : IDisposable
/// The frame metadata.
/// The to encode the image data to.
/// Flag indicating, if an animation parameter is present.
- public void Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
+ /// A indicating whether the frame contains an alpha channel.
+ public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
// Convert image pixels to bgra array.
@@ -324,6 +330,8 @@ internal class Vp8LEncoder : IDisposable
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}
+
+ return hasAlpha;
}
///
@@ -502,7 +510,7 @@ internal class Vp8LEncoder : IDisposable
/// The type of the pixels.
/// The frame pixel buffer to convert.
/// true, if the image is non opaque.
- private bool ConvertPixelsToBgra(Buffer2DRegion pixels)
+ public bool ConvertPixelsToBgra(Buffer2DRegion pixels)
where TPixel : unmanaged, IPixel
{
bool nonOpaque = false;
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 2b74c300a..7317527f7 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -311,7 +311,7 @@ internal class Vp8Encoder : IDisposable
///
private int MbHeaderLimit { get; }
- public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation)
+ public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
// Write bytes from the bitwriter buffer to the stream.
@@ -321,7 +321,7 @@ internal class Vp8Encoder : IDisposable
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
- BitWriterBase.WriteTrunksBeforeData(
+ WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)image.Width,
(uint)image.Height,
@@ -336,9 +336,11 @@ internal class Vp8Encoder : IDisposable
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
+
+ return vp8x;
}
- public void EncodeFooter(Image image, Stream stream)
+ public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel
{
// Write bytes from the bitwriter buffer to the stream.
@@ -347,7 +349,9 @@ internal class Vp8Encoder : IDisposable
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
- BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ bool updateVp8x = hasAlpha && vp8x != default;
+ WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x;
+ BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile);
}
///
@@ -358,9 +362,10 @@ internal class Vp8Encoder : IDisposable
/// The stream to encode the image data to.
/// The region of interest within the frame to encode.
/// The frame metadata.
- public void EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
- where TPixel : unmanaged, IPixel =>
- this.Encode(stream, frame, bounds, frameMetadata, true, null);
+ /// A indicating whether the frame contains an alpha channel.
+ public bool EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
+ where TPixel : unmanaged, IPixel
+ => this.Encode(stream, frame, bounds, frameMetadata, true, null);
///
/// Encodes the static image frame to the specified stream.
@@ -385,7 +390,8 @@ internal class Vp8Encoder : IDisposable
/// The frame metadata.
/// Flag indicating, if an animation parameter is present.
/// The image to encode from.
- private void Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image)
+ /// A indicating whether the frame contains an alpha channel.
+ private bool Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image)
where TPixel : unmanaged, IPixel
{
int width = bounds.Width;
@@ -515,6 +521,8 @@ internal class Vp8Encoder : IDisposable
{
encodedAlphaData?.Dispose();
}
+
+ return hasAlpha;
}
///
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
index 80ffe8a99..07f09d45e 100644
--- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
+++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
-using System.Drawing;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 69a0afcd9..21a25860c 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -54,7 +54,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
///
/// The flag to decide how to handle the background color in the Animation Chunk.
///
- private BackgroundColorHandling backgroundColorHandling;
+ private readonly BackgroundColorHandling backgroundColorHandling;
///
/// Initializes a new instance of the class.
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index e37c1d179..d29759f9a 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory;
@@ -143,12 +144,14 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.nearLossless,
this.nearLosslessQuality);
- encoder.EncodeHeader(image, stream, hasAnimation);
+ long initialPosition = stream.Position;
+ bool hasAlpha = false;
+ WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation);
// Encode the first frame.
ImageFrame previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame);
- encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
+ hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
if (hasAnimation)
{
@@ -190,14 +193,14 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.nearLossless,
this.nearLosslessQuality);
- animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
+ hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
}
}
- encoder.EncodeFooter(image, stream);
+ encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}
else
{
@@ -214,17 +217,20 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.spatialNoiseShaping,
this.alphaCompression);
+ long initialPosition = stream.Position;
+ bool hasAlpha = false;
+ WebpVp8X vp8x = default;
if (image.Frames.Count > 1)
{
- // TODO: What about alpha here?
- encoder.EncodeHeader(image, stream, false, true);
+ // The alpha flag is updated following encoding.
+ vp8x = encoder.EncodeHeader(image, stream, false, true);
// Encode the first frame.
ImageFrame previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame);
WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
- encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
+ hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@@ -263,18 +269,19 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.spatialNoiseShaping,
this.alphaCompression);
- animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
+ hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
}
+
+ encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}
else
{
encoder.EncodeStatic(stream, image);
+ encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}
-
- encoder.EncodeFooter(image, stream);
}
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 9b165526e..9ff368d4e 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -87,7 +87,9 @@ public partial class PngDecoderTests
TestImages.Png.DisposeBackgroundRegion,
TestImages.Png.DisposePreviousFirst,
TestImages.Png.DisposeBackgroundBeforeRegion,
- TestImages.Png.BlendOverMultiple
+ TestImages.Png.BlendOverMultiple,
+ TestImages.Png.FrameOffset,
+ TestImages.Png.DefaultNotAnimated
};
[Theory]
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
index 044da2193..76fd260dd 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 partial class PngEncoderTests
}
}
+ [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.AnimateRootFrame ? 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/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 950f1d2e3..ca5aae961 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -447,6 +447,8 @@ public partial class PngEncoderTests
[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 partial class PngEncoderTests
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.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 b3c122a7a..225e4deef 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -32,7 +32,8 @@ public class PngMetadataTests
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List { new PngTextData("name", "value", "foo", "bar") },
- RepeatCount = 123
+ RepeatCount = 123,
+ AnimateRootFrame = false
};
PngMetadata clone = (PngMetadata)meta.DeepClone();
@@ -44,6 +45,7 @@ public class PngMetadataTests
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.True(meta.RepeatCount == clone.RepeatCount);
+ Assert.True(meta.AnimateRootFrame == clone.AnimateRootFrame);
clone.BitDepth = PngBitDepth.Bit2;
clone.ColorType = PngColorType.Palette;
@@ -144,6 +146,26 @@ public class PngMetadataTests
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.AnimateRootFrame);
+ }
+
+ [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.AnimateRootFrame);
+ }
+
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 42778e6f1..a4e74c516 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -73,6 +73,8 @@ public static class TestImages
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 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 000000000..4c5ea8169
--- /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 000000000..790fe45e4
--- /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/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 000000000..870ed61a4
--- /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: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
new file mode 100644
index 000000000..cab85d946
--- /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: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
new file mode 100644
index 000000000..1a2c5adcf
--- /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: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
new file mode 100644
index 000000000..d850459ee
--- /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: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
new file mode 100644
index 000000000..000b0567d
--- /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:2db38d7ffcc95c23a5c94a06f10c6cc67406ae581a955c99ede4af97b1a044f8
+size 628
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 000000000..1ed72698d
--- /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
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 000000000..4eebb44a3
--- /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