Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

APNG support #2511

Merged
merged 25 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
45f6f5b
Implement APNG decoder
Poker-sang Aug 12, 2023
7b6c32d
implement APNG encoder
Poker-sang Aug 12, 2023
01caebd
Add UnitTest
Poker-sang Aug 13, 2023
6f9525e
Merge branch 'main' into main
Poker-sang Aug 15, 2023
64a0ff0
Fix simple issues in review
Poker-sang Aug 17, 2023
e7eed49
Merge branch 'main' of github.com:Poker-sang/ImageSharp
Poker-sang Aug 17, 2023
bf308b7
Merge branch 'main' into main
Poker-sang Aug 17, 2023
6e54822
Fix review
Poker-sang Aug 17, 2023
c253f39
Fix offset
Poker-sang Aug 17, 2023
1464064
Fix: replace lambda with method
Poker-sang Aug 17, 2023
316a839
Optimize code
Poker-sang Aug 18, 2023
a6b8abe
remove set to null from disposal
Poker-sang Aug 22, 2023
b0dc908
Merge branch 'main' into main
JimBobSquarePants Sep 12, 2023
5a711a8
Merge remote-tracking branch 'upstream/main'
JimBobSquarePants Oct 17, 2023
aada974
Refactor and cleanup
JimBobSquarePants Oct 17, 2023
564c3d1
Fix encoding
JimBobSquarePants Oct 17, 2023
3bc12e4
Fix failing tests
JimBobSquarePants Oct 17, 2023
0385ad0
Fix header bit depth assignment.
JimBobSquarePants Oct 19, 2023
5ed6f24
Reintroduce scanline optimizations
JimBobSquarePants Oct 23, 2023
bc5b6c5
Add alpha blending support
JimBobSquarePants Oct 23, 2023
8455275
Handle disposal methods.
JimBobSquarePants Oct 23, 2023
56588d3
Use region for alpha blending
JimBobSquarePants Oct 23, 2023
66f444d
Fix alpha blending and add tests
JimBobSquarePants Oct 30, 2023
14a95a8
Rename properties and add metadata tests
JimBobSquarePants Oct 31, 2023
b4e9805
Update PngDecoderTests.cs
JimBobSquarePants Oct 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ public override int ReadByte()
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
if (this.currentDataRemaining == 0)
if (this.currentDataRemaining is 0)
{
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
this.currentDataRemaining = this.getData();

if (this.currentDataRemaining == 0)
if (this.currentDataRemaining is 0)
{
return 0;
}
Expand All @@ -142,11 +142,11 @@ public override int Read(byte[] buffer, int offset, int count)
// Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0;
offset += totalBytesRead;
while (this.currentDataRemaining == 0 && totalBytesRead < count)
while (this.currentDataRemaining is 0 && totalBytesRead < count)
{
this.currentDataRemaining = this.getData();

if (this.currentDataRemaining == 0)
if (this.currentDataRemaining is 0)
{
return totalBytesRead;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public Configuration()
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
/// <param name="configurationModules">A collection of configuration modules to register.</param>
public Configuration(params IImageFormatConfigurationModule[] configurationModules)
public Configuration(params IImageFormatConfigurationModule[]? configurationModules)
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
if (configurationModules != null)
{
Expand Down
20 changes: 20 additions & 0 deletions src/ImageSharp/Formats/Png/APngBlendOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Png;

/// <summary>
/// Specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer.
/// </summary>
public enum APngBlendOperation
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
/// </summary>
Source,

/// <summary>
/// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. Note that the second variation of the sample code is applicable.
/// </summary>
Over
}
25 changes: 25 additions & 0 deletions src/ImageSharp/Formats/Png/APngDisposeOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Png;

/// <summary>
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
/// </summary>
public enum APngDisposeOperation
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
/// </summary>
None,

/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
/// </summary>
Background,

/// <summary>
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
/// </summary>
Previous
}
94 changes: 94 additions & 0 deletions src/ImageSharp/Formats/Png/APngFrameMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Formats.Png.Chunks;

namespace SixLabors.ImageSharp.Formats.Png;

/// <summary>
/// Provides APng specific metadata information for the image frame.
/// </summary>
public class APngFrameMetadata : IDeepCloneable
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
/// </summary>
public APngFrameMetadata()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private APngFrameMetadata(APngFrameMetadata other)
{
this.Width = other.Width;
this.Height = other.Height;
this.XOffset = other.XOffset;
this.YOffset = other.YOffset;
this.DelayNumber = other.DelayNumber;
this.DelayDenominator = other.DelayDenominator;
this.DisposeOperation = other.DisposeOperation;
this.BlendOperation = other.BlendOperation;
}

/// <summary>
/// Gets or sets the width of the following frame
/// </summary>
public int Width { get; set; }

/// <summary>
/// Gets or sets the height of the following frame
/// </summary>
public int Height { get; set; }

/// <summary>
/// Gets or sets the X position at which to render the following frame
/// </summary>
public int XOffset { get; set; }

/// <summary>
/// Gets or sets the Y position at which to render the following frame
/// </summary>
public int YOffset { get; set; }

/// <summary>
/// Gets or sets the frame delay fraction numerator
/// </summary>
public short DelayNumber { get; set; }

/// <summary>
/// Gets or sets the frame delay fraction denominator
/// </summary>
public short DelayDenominator { get; set; }

/// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame
/// </summary>
public APngDisposeOperation DisposeOperation { get; set; }

/// <summary>
/// Gets or sets the type of frame area rendering for this frame
/// </summary>
public APngBlendOperation BlendOperation { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
/// </summary>
/// <param name="frameControl">The chunk to create an instance from.</param>
internal void FromChunk(APngFrameControl frameControl)
{
this.Width = frameControl.Width;
this.Height = frameControl.Height;
this.XOffset = frameControl.XOffset;
this.YOffset = frameControl.YOffset;
this.DelayNumber = frameControl.DelayNumber;
this.DelayDenominator = frameControl.DelayDenominator;
this.DisposeOperation = frameControl.DisposeOperation;
this.BlendOperation = frameControl.BlendOperation;
}

/// <inheritdoc/>
public IDeepCloneable DeepClone() => new APngFrameMetadata(this);
}
43 changes: 43 additions & 0 deletions src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Png.Chunks;

internal record APngAnimationControl(
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
int NumberFrames,
int NumberPlays)
{
public const int Size = 8;

/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; } = NumberFrames;
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; } = NumberPlays;
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Writes the acTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
}

/// <summary>
/// Parses the APngAnimationControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed acTL.</returns>
public static APngAnimationControl Parse(ReadOnlySpan<byte> data)
=> new(
NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
}
172 changes: 172 additions & 0 deletions src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Png.Chunks;

internal readonly struct APngFrameControl
{
public const int Size = 26;

public APngFrameControl(
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
int sequenceNumber,
int width,
int height,
int xOffset,
int yOffset,
short delayNumber,
short delayDenominator,
APngDisposeOperation disposeOperation,
APngBlendOperation blendOperation)
{
this.SequenceNumber = sequenceNumber;
this.Width = width;
this.Height = height;
this.XOffset = xOffset;
this.YOffset = yOffset;
this.DelayNumber = delayNumber;
this.DelayDenominator = delayDenominator;
this.DisposeOperation = disposeOperation;
this.BlendOperation = blendOperation;
}

/// <summary>
/// Gets the sequence number of the animation chunk, starting from 0
/// </summary>
public int SequenceNumber { get; }

/// <summary>
/// Gets the width of the following frame
/// </summary>
public int Width { get; }

/// <summary>
/// Gets the height of the following frame
/// </summary>
public int Height { get; }

/// <summary>
/// Gets the X position at which to render the following frame
/// </summary>
public int XOffset { get; }

/// <summary>
/// Gets the Y position at which to render the following frame
/// </summary>
public int YOffset { get; }

/// <summary>
/// Gets the frame delay fraction numerator
/// </summary>
public short DelayNumber { get; }

/// <summary>
/// Gets the frame delay fraction denominator
/// </summary>
public short DelayDenominator { get; }

/// <summary>
/// Gets the type of frame area disposal to be done after rendering this frame
/// </summary>
public APngDisposeOperation DisposeOperation { get; }

/// <summary>
/// Gets the type of frame area rendering for this frame
/// </summary>
public APngBlendOperation BlendOperation { get; }

/// <summary>
/// Validates the APng fcTL.
/// </summary>
/// <exception cref="NotSupportedException">
/// Thrown if the image does pass validation.
/// </exception>
public void Validate(PngHeader hdr)
{
if (this.XOffset < 0)
{
throw new NotSupportedException($"Invalid XOffset. Expected >= 0. Was '{this.XOffset}'.");
}

if (this.YOffset < 0)
{
throw new NotSupportedException($"Invalid YOffset. Expected >= 0. Was '{this.YOffset}'.");
}

if (this.Width <= 0)
{
throw new NotSupportedException($"Invalid Width. Expected > 0. Was '{this.Width}'.");
}

if (this.Height <= 0)
{
throw new NotSupportedException($"Invalid Height. Expected > 0. Was '{this.Height}'.");
}

if (this.XOffset + this.Width > hdr.Width)
{
throw new NotSupportedException($"Invalid XOffset or Width. The sum > PngHeader.Width. Was '{this.XOffset + this.Width}'.");
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
}

if (this.YOffset + this.Height > hdr.Height)
{
throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'.");
}
}

/// <summary>
/// Parses the APngFrameControl from the given metadata.
/// </summary>
/// <param name="frameMetadata">The metadata to parse.</param>
/// <param name="sequenceNumber">Sequence number.</param>
public static APngFrameControl FromMetadata(APngFrameMetadata frameMetadata, int sequenceNumber)
{
APngFrameControl fcTL = new(
sequenceNumber,
frameMetadata.Width,
frameMetadata.Height,
frameMetadata.XOffset,
frameMetadata.YOffset,
frameMetadata.DelayNumber,
frameMetadata.DelayDenominator,
frameMetadata.DisposeOperation,
frameMetadata.BlendOperation);
return fcTL;
}

/// <summary>
/// Writes the fcTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber);
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width);
BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height);
BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset);
BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset);
BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber);
BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator);

buffer[24] = (byte)this.DisposeOperation;
buffer[25] = (byte)this.BlendOperation;
}

/// <summary>
/// Parses the APngFrameControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed fcTL.</returns>
public static APngFrameControl Parse(ReadOnlySpan<byte> data)
=> new(
sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]),
height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]),
xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]),
yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]),
delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]),
delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]),
disposeOperation: (APngDisposeOperation)data[24],
blendOperation: (APngBlendOperation)data[25]);
}