Skip to content

Commit 45f6f5b

Browse files
committed
Implement APNG decoder
1 parent 900e9d0 commit 45f6f5b

22 files changed

+707
-118
lines changed

src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ public override int ReadByte()
123123
/// <inheritdoc/>
124124
public override int Read(byte[] buffer, int offset, int count)
125125
{
126-
if (this.currentDataRemaining == 0)
126+
if (this.currentDataRemaining is 0)
127127
{
128128
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
129129
this.currentDataRemaining = this.getData();
130130

131-
if (this.currentDataRemaining == 0)
131+
if (this.currentDataRemaining is 0)
132132
{
133133
return 0;
134134
}
@@ -142,11 +142,11 @@ public override int Read(byte[] buffer, int offset, int count)
142142
// Keep reading data until we've reached the end of the stream or filled the buffer.
143143
int bytesRead = 0;
144144
offset += totalBytesRead;
145-
while (this.currentDataRemaining == 0 && totalBytesRead < count)
145+
while (this.currentDataRemaining is 0 && totalBytesRead < count)
146146
{
147147
this.currentDataRemaining = this.getData();
148148

149-
if (this.currentDataRemaining == 0)
149+
if (this.currentDataRemaining is 0)
150150
{
151151
return totalBytesRead;
152152
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Png;
5+
6+
/// <summary>
7+
/// 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.
8+
/// </summary>
9+
public enum APngBlendOperation
10+
{
11+
/// <summary>
12+
/// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
13+
/// </summary>
14+
Source,
15+
16+
/// <summary>
17+
/// 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.
18+
/// </summary>
19+
Over
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Png;
5+
6+
/// <summary>
7+
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
8+
/// </summary>
9+
public enum APngDisposeOperation
10+
{
11+
/// <summary>
12+
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
13+
/// </summary>
14+
None,
15+
16+
/// <summary>
17+
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
18+
/// </summary>
19+
Background,
20+
21+
/// <summary>
22+
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
23+
/// </summary>
24+
Previous
25+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Formats.Png.Chunks;
5+
6+
namespace SixLabors.ImageSharp.Formats.Png;
7+
8+
/// <summary>
9+
/// Provides APng specific metadata information for the image frame.
10+
/// </summary>
11+
public class APngFrameMetadata : IDeepCloneable
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
15+
/// </summary>
16+
public APngFrameMetadata()
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
22+
/// </summary>
23+
/// <param name="other">The metadata to create an instance from.</param>
24+
private APngFrameMetadata(APngFrameMetadata other)
25+
{
26+
this.Width = other.Width;
27+
this.Height = other.Height;
28+
this.XOffset = other.XOffset;
29+
this.YOffset = other.YOffset;
30+
this.DelayNumber = other.DelayNumber;
31+
this.DelayDenominator = other.DelayDenominator;
32+
this.DisposeOperation = other.DisposeOperation;
33+
this.BlendOperation = other.BlendOperation;
34+
}
35+
36+
/// <summary>
37+
/// Gets or sets the width of the following frame
38+
/// </summary>
39+
public int Width { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the height of the following frame
43+
/// </summary>
44+
public int Height { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the X position at which to render the following frame
48+
/// </summary>
49+
public int XOffset { get; set; }
50+
51+
/// <summary>
52+
/// Gets or sets the Y position at which to render the following frame
53+
/// </summary>
54+
public int YOffset { get; set; }
55+
56+
/// <summary>
57+
/// Gets or sets the frame delay fraction numerator
58+
/// </summary>
59+
public short DelayNumber { get; set; }
60+
61+
/// <summary>
62+
/// Gets or sets the frame delay fraction denominator
63+
/// </summary>
64+
public short DelayDenominator { get; set; }
65+
66+
/// <summary>
67+
/// Gets or sets the type of frame area disposal to be done after rendering this frame
68+
/// </summary>
69+
public APngDisposeOperation DisposeOperation { get; set; }
70+
71+
/// <summary>
72+
/// Gets or sets the type of frame area rendering for this frame
73+
/// </summary>
74+
public APngBlendOperation BlendOperation { get; set; }
75+
76+
/// <summary>
77+
/// Initializes a new instance of the <see cref="APngFrameMetadata"/> class.
78+
/// </summary>
79+
/// <param name="frameControl">The chunk to create an instance from.</param>
80+
internal void FromChunk(APngFrameControl frameControl)
81+
{
82+
this.Width = frameControl.Width;
83+
this.Height = frameControl.Height;
84+
this.XOffset = frameControl.XOffset;
85+
this.YOffset = frameControl.YOffset;
86+
this.DelayNumber = frameControl.DelayNumber;
87+
this.DelayDenominator = frameControl.DelayDenominator;
88+
this.DisposeOperation = frameControl.DisposeOperation;
89+
this.BlendOperation = frameControl.BlendOperation;
90+
}
91+
92+
/// <inheritdoc/>
93+
public IDeepCloneable DeepClone() => new APngFrameMetadata(this);
94+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers.Binary;
5+
6+
namespace SixLabors.ImageSharp.Formats.Png.Chunks;
7+
8+
internal record APngAnimationControl(
9+
int NumberFrames,
10+
int NumberPlays)
11+
{
12+
public const int Size = 8;
13+
14+
/// <summary>
15+
/// Gets the number of frames
16+
/// </summary>
17+
public int NumberFrames { get; } = NumberFrames;
18+
19+
/// <summary>
20+
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
21+
/// </summary>
22+
public int NumberPlays { get; } = NumberPlays;
23+
24+
/// <summary>
25+
/// Writes the acTL to the given buffer.
26+
/// </summary>
27+
/// <param name="buffer">The buffer to write to.</param>
28+
public void WriteTo(Span<byte> buffer)
29+
{
30+
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
31+
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
32+
}
33+
34+
/// <summary>
35+
/// Parses the APngAnimationControl from the given data buffer.
36+
/// </summary>
37+
/// <param name="data">The data to parse.</param>
38+
/// <returns>The parsed acTL.</returns>
39+
public static APngAnimationControl Parse(ReadOnlySpan<byte> data)
40+
=> new(
41+
NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
42+
NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
43+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers.Binary;
5+
6+
namespace SixLabors.ImageSharp.Formats.Png.Chunks;
7+
8+
internal readonly struct APngFrameControl
9+
{
10+
public const int Size = 26;
11+
12+
public APngFrameControl(
13+
int sequenceNumber,
14+
int width,
15+
int height,
16+
int xOffset,
17+
int yOffset,
18+
short delayNumber,
19+
short delayDenominator,
20+
APngDisposeOperation disposeOperation,
21+
APngBlendOperation blendOperation)
22+
{
23+
this.SequenceNumber = sequenceNumber;
24+
this.Width = width;
25+
this.Height = height;
26+
this.XOffset = xOffset;
27+
this.YOffset = yOffset;
28+
this.DelayNumber = delayNumber;
29+
this.DelayDenominator = delayDenominator;
30+
this.DisposeOperation = disposeOperation;
31+
this.BlendOperation = blendOperation;
32+
}
33+
34+
/// <summary>
35+
/// Gets the sequence number of the animation chunk, starting from 0
36+
/// </summary>
37+
public int SequenceNumber { get; }
38+
39+
/// <summary>
40+
/// Gets the width of the following frame
41+
/// </summary>
42+
public int Width { get; }
43+
44+
/// <summary>
45+
/// Gets the height of the following frame
46+
/// </summary>
47+
public int Height { get; }
48+
49+
/// <summary>
50+
/// Gets the X position at which to render the following frame
51+
/// </summary>
52+
public int XOffset { get; }
53+
54+
/// <summary>
55+
/// Gets the Y position at which to render the following frame
56+
/// </summary>
57+
public int YOffset { get; }
58+
59+
/// <summary>
60+
/// Gets the frame delay fraction numerator
61+
/// </summary>
62+
public short DelayNumber { get; }
63+
64+
/// <summary>
65+
/// Gets the frame delay fraction denominator
66+
/// </summary>
67+
public short DelayDenominator { get; }
68+
69+
/// <summary>
70+
/// Gets the type of frame area disposal to be done after rendering this frame
71+
/// </summary>
72+
public APngDisposeOperation DisposeOperation { get; }
73+
74+
/// <summary>
75+
/// Gets the type of frame area rendering for this frame
76+
/// </summary>
77+
public APngBlendOperation BlendOperation { get; }
78+
79+
/// <summary>
80+
/// Validates the APng fcTL.
81+
/// </summary>
82+
/// <exception cref="NotSupportedException">
83+
/// Thrown if the image does pass validation.
84+
/// </exception>
85+
public void Validate(PngHeader hdr)
86+
{
87+
if (this.XOffset < 0)
88+
{
89+
throw new NotSupportedException($"Invalid XOffset. Expected >= 0. Was '{this.XOffset}'.");
90+
}
91+
92+
if (this.YOffset < 0)
93+
{
94+
throw new NotSupportedException($"Invalid YOffset. Expected >= 0. Was '{this.YOffset}'.");
95+
}
96+
97+
if (this.Width <= 0)
98+
{
99+
throw new NotSupportedException($"Invalid Width. Expected > 0. Was '{this.Width}'.");
100+
}
101+
102+
if (this.Height <= 0)
103+
{
104+
throw new NotSupportedException($"Invalid Height. Expected > 0. Was '{this.Height}'.");
105+
}
106+
107+
if (this.XOffset + this.Width > hdr.Width)
108+
{
109+
throw new NotSupportedException($"Invalid XOffset or Width. The sum > PngHeader.Width. Was '{this.XOffset + this.Width}'.");
110+
}
111+
112+
if (this.YOffset + this.Height > hdr.Height)
113+
{
114+
throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'.");
115+
}
116+
}
117+
118+
/// <summary>
119+
/// Writes the fcTL to the given buffer.
120+
/// </summary>
121+
/// <param name="buffer">The buffer to write to.</param>
122+
public void WriteTo(Span<byte> buffer)
123+
{
124+
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber);
125+
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width);
126+
BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height);
127+
BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset);
128+
BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset);
129+
BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber);
130+
BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator);
131+
132+
buffer[24] = (byte)this.DisposeOperation;
133+
buffer[25] = (byte)this.BlendOperation;
134+
}
135+
136+
/// <summary>
137+
/// Parses the APngFrameControl from the given data buffer.
138+
/// </summary>
139+
/// <param name="data">The data to parse.</param>
140+
/// <returns>The parsed fcTL.</returns>
141+
public static APngFrameControl Parse(ReadOnlySpan<byte> data)
142+
=> new(
143+
sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
144+
width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]),
145+
height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]),
146+
xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]),
147+
yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]),
148+
delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]),
149+
delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]),
150+
disposeOperation: (APngDisposeOperation)data[24],
151+
blendOperation: (APngBlendOperation)data[25]);
152+
}

src/ImageSharp/Formats/Png/PngHeader.cs renamed to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
using System.Buffers.Binary;
66

7-
namespace SixLabors.ImageSharp.Formats.Png;
7+
namespace SixLabors.ImageSharp.Formats.Png.Chunks;
88

99
/// <summary>
1010
/// Represents the png header chunk.

0 commit comments

Comments
 (0)