Browse Source

Merge branch 'release/3.1.x' into af/memlimit-01

pull/2706/head
James Jackson-South 2 years ago
parent
commit
eec9718f41
  1. 17
      src/ImageSharp/Common/Helpers/RiffHelper.cs
  2. 37
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  3. 45
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 6
      src/ImageSharp/Formats/Png/PngMetadata.cs
  5. 3
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  6. 26
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  7. 23
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  8. 20
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  9. 24
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  10. 1
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  11. 2
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  12. 27
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  13. 4
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  14. 33
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  15. 8
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  16. 24
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  17. 2
      tests/ImageSharp.Tests/TestImages.cs
  18. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/00.png
  19. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_default-not-animated.png/01.png
  20. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/00.png
  21. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/01.png
  22. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/02.png
  23. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/03.png
  24. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_frame-offset.png/04.png
  25. 3
      tests/Images/Input/Png/animated/default-not-animated.png
  26. 3
      tests/Images/Input/Png/animated/frame-offset.png

17
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;
}
}
}

37
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
/// <param name="previousFrame">The previous frame.</param>
/// <param name="frame">The created frame</param>
private void InitializeFrame<TPixel>(
FrameControl previousFrameControl,
FrameControl? previousFrameControl,
FrameControl currentFrameControl,
Image<TPixel> image,
ImageFrame<TPixel>? 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<TPixel> 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<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
Rectangle restoreArea = previousFrameControl.Value.Bounds;
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
pixelRegion.Clear();
}

45
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -161,6 +161,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ImageFrame<TPixel>? clonedFrame = null;
ImageFrame<TPixel> 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<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
uint increment = 0;
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? 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);

6
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
/// </summary>
public uint RepeatCount { get; set; } = 1;
/// <summary>
/// Gets or sets a value indicating whether the root frame is shown as part of the animated sequence
/// </summary>
public bool AnimateRootFrame { get; set; } = true;
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);

3
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());

26
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -88,7 +88,8 @@ internal abstract class BitWriterBase
/// <param name="iccProfile">The color profile.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public static void WriteTrunksBeforeData(
/// <returns>A <see cref="WebpVp8X"/> or a default instance.</returns>
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;
}
/// <summary>
@ -124,10 +128,16 @@ internal abstract class BitWriterBase
/// Write the trunks after data trunk.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="vp8x">The VP8X chunk.</param>
/// <param name="updateVp8x">Whether to update the chunk.</param>
/// <param name="initialPosition">The initial position of the stream before encoding.</param>
/// <param name="exifProfile">The EXIF profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
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);
}
/// <summary>
@ -186,19 +196,21 @@ internal abstract class BitWriterBase
/// Writes a VP8X header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="exifProfile">An EXIF profile or null, if it does not exist.</param>
/// <param name="xmpProfile">An XMP profile or null, if it does not exist.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
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;
}
}

23
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<WebpVp8X>
{
public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
{
@ -53,6 +53,24 @@ internal readonly struct WebpVp8X
/// </summary>
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;

20
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -237,7 +237,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public Vp8LHashChain HashChain { get; }
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeFooter<TPixel>(Image<TPixel> image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
@ -285,7 +290,8 @@ internal class Vp8LEncoder : IDisposable
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public void Encode<TPixel>(ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
public bool Encode<TPixel>(ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// Convert image pixels to bgra array.
@ -324,6 +330,8 @@ internal class Vp8LEncoder : IDisposable
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}
return hasAlpha;
}
/// <summary>
@ -502,7 +510,7 @@ internal class Vp8LEncoder : IDisposable
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="pixels">The frame pixel buffer to convert.</param>
/// <returns>true, if the image is non opaque.</returns>
private bool ConvertPixelsToBgra<TPixel>(Buffer2DRegion<TPixel> pixels)
public bool ConvertPixelsToBgra<TPixel>(Buffer2DRegion<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
bool nonOpaque = false;

24
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -311,7 +311,7 @@ internal class Vp8Encoder : IDisposable
/// </summary>
private int MbHeaderLimit { get; }
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeFooter<TPixel>(Image<TPixel> image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
@ -358,9 +362,10 @@ internal class Vp8Encoder : IDisposable
/// <param name="stream">The stream to encode the image data to.</param>
/// <param name="bounds">The region of interest within the frame to encode.</param>
/// <param name="frameMetadata">The frame metadata.</param>
public void EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(stream, frame, bounds, frameMetadata, true, null);
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
public bool EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
where TPixel : unmanaged, IPixel<TPixel>
=> this.Encode(stream, frame, bounds, frameMetadata, true, null);
/// <summary>
/// Encodes the static image frame to the specified stream.
@ -385,7 +390,8 @@ internal class Vp8Encoder : IDisposable
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
/// <param name="image">The image to encode from.</param>
private void Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image<TPixel> image)
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
private bool Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = bounds.Width;
@ -515,6 +521,8 @@ internal class Vp8Encoder : IDisposable
{
encodedAlphaData?.Dispose();
}
return hasAlpha;
}
/// <inheritdoc/>

1
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;

2
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -54,7 +54,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <summary>
/// The flag to decide how to handle the background color in the Animation Chunk.
/// </summary>
private BackgroundColorHandling backgroundColorHandling;
private readonly BackgroundColorHandling backgroundColorHandling;
/// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.

27
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<TPixel> 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<TPixel> 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);
}
}
}

4
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]

33
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<byte> 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)]

8
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -458,15 +460,17 @@ public partial class PngEncoderTests
image.DebugSave(provider: provider, encoder: PngEncoder, null, false);
using Image<Rgba32> output = Image.Load<Rgba32>(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++)
{

24
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -32,7 +32,8 @@ public class PngMetadataTests
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List<PngTextData> { 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)

2
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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
Loading…
Cancel
Save