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’ll 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 9 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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
47 changes: 47 additions & 0 deletions src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Png.Chunks;

internal readonly struct AnimationControl
{
public const int Size = 8;

public AnimationControl(int numberFrames, int numberPlays)
{
this.NumberFrames = numberFrames;
this.NumberPlays = numberPlays;
}

/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; }
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; }
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 AnimationControl Parse(ReadOnlySpan<byte> data)
=> new(
numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
}
182 changes: 182 additions & 0 deletions src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Png.Chunks;

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

public FrameControl(
int sequenceNumber,
int width,
int height,
int xOffset,
int yOffset,
short delayNumber,
short delayDenominator,
PngDisposeOperation disposeOperation,
PngBlendOperation 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 X limit at which to render the following frame
/// </summary>
public uint XLimit => (uint)(this.XOffset + this.Width);

/// <summary>
/// Gets the Y limit at which to render the following frame
/// </summary>
public uint YLimit => (uint)(this.YOffset + this.Height);

/// <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 PngDisposeOperation DisposeOperation { get; }

/// <summary>
/// Gets the type of frame area rendering for this frame
/// </summary>
public PngBlendOperation 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)
{
PngThrowHelper.ThrowInvalidParameter(this.XOffset, "Expected >= 0");
}

if (this.YOffset < 0)
{
PngThrowHelper.ThrowInvalidParameter(this.YOffset, "Expected >= 0");
}

if (this.Width <= 0)
{
PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0");
}

if (this.Height <= 0)
{
PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0");
}

if (this.XLimit > hdr.Width)
{
PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
}

if (this.YLimit > hdr.Height)
{
PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.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 FrameControl FromMetadata(PngFrameMetadata frameMetadata, int sequenceNumber)
{
FrameControl 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 FrameControl 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: (PngDisposeOperation)data[24],
blendOperation: (PngBlendOperation)data[25]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Formats.Png.Chunks;

/// <summary>
/// Represents the png header chunk.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers.Binary;
Expand All @@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
internal readonly struct PhysicalChunkData
internal readonly struct PngPhysical
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
public const int Size = 9;

public PhysicalChunkData(uint x, uint y, byte unitSpecifier)
public PngPhysical(uint x, uint y, byte unitSpecifier)
{
this.XAxisPixelsPerUnit = x;
this.YAxisPixelsPerUnit = y;
Expand Down Expand Up @@ -44,13 +44,13 @@ public PhysicalChunkData(uint x, uint y, byte unitSpecifier)
/// </summary>
/// <param name="data">The data buffer.</param>
/// <returns>The parsed PhysicalChunkData.</returns>
public static PhysicalChunkData Parse(ReadOnlySpan<byte> data)
public static PngPhysical Parse(ReadOnlySpan<byte> data)
{
uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]);
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8];

return new PhysicalChunkData(hResolution, vResolution, unit);
return new PngPhysical(hResolution, vResolution, unit);
}

/// <summary>
Expand All @@ -59,7 +59,7 @@ public static PhysicalChunkData Parse(ReadOnlySpan<byte> data)
/// </summary>
/// <param name="meta">The metadata.</param>
/// <returns>The constructed PngPhysicalChunkData instance.</returns>
public static PhysicalChunkData FromMetadata(ImageMetadata meta)
public static PngPhysical FromMetadata(ImageMetadata meta)
{
byte unitSpecifier = 0;
uint x;
Expand Down Expand Up @@ -92,7 +92,7 @@ public static PhysicalChunkData FromMetadata(ImageMetadata meta)
break;
}

return new PhysicalChunkData(x, y, unitSpecifier);
return new PngPhysical(x, y, unitSpecifier);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Formats.Png.Chunks;

/// <summary>
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks.
Expand Down
20 changes: 18 additions & 2 deletions src/ImageSharp/Formats/Png/MetadataExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;

Expand All @@ -14,7 +15,22 @@ public static partial class MetadataExtensions
/// <summary>
/// Gets the png format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngMetadata"/>.</returns>
public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance);
public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);

/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);

/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
}
20 changes: 20 additions & 0 deletions src/ImageSharp/Formats/Png/PngBlendOperation.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 PngBlendOperation
{
/// <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
}
7 changes: 4 additions & 3 deletions src/ImageSharp/Formats/Png/PngChunk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public PngChunk(int length, PngChunkType type, IMemoryOwner<byte> data = null)
/// Gets a value indicating whether the given chunk is critical to decoding
/// </summary>
public bool IsCritical =>
this.Type == PngChunkType.Header ||
this.Type == PngChunkType.Palette ||
this.Type == PngChunkType.Data;
this.Type is PngChunkType.Header or
PngChunkType.Palette or
PngChunkType.Data or
PngChunkType.FrameData;
}
Loading