diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
index 06a7c3928c..c9f9904363 100644
--- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
+++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
@@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream
///
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;
}
@@ -142,11 +142,11 @@ internal sealed class ZlibInflateStream : Stream
// 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;
}
diff --git a/src/ImageSharp/Formats/Png/APngBlendOperation.cs b/src/ImageSharp/Formats/Png/APngBlendOperation.cs
new file mode 100644
index 0000000000..0e8cdb4289
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/APngBlendOperation.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// 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.
+///
+public enum APngBlendOperation
+{
+ ///
+ /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
+ ///
+ Source,
+
+ ///
+ /// 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.
+ ///
+ Over
+}
diff --git a/src/ImageSharp/Formats/Png/APngDisposeOperation.cs b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs
new file mode 100644
index 0000000000..7b39a220d3
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
+///
+public enum APngDisposeOperation
+{
+ ///
+ /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
+ ///
+ None,
+
+ ///
+ /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
+ ///
+ Background,
+
+ ///
+ /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
+ ///
+ Previous
+}
diff --git a/src/ImageSharp/Formats/Png/APngFrameMetadata.cs b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs
new file mode 100644
index 0000000000..f4f5fec916
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs
@@ -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;
+
+///
+/// Provides APng specific metadata information for the image frame.
+///
+public class APngFrameMetadata : IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public APngFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ 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;
+ }
+
+ ///
+ /// Gets or sets the width of the following frame
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets the height of the following frame
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets the X position at which to render the following frame
+ ///
+ public int XOffset { get; set; }
+
+ ///
+ /// Gets or sets the Y position at which to render the following frame
+ ///
+ public int YOffset { get; set; }
+
+ ///
+ /// Gets or sets the frame delay fraction numerator
+ ///
+ public short DelayNumber { get; set; }
+
+ ///
+ /// Gets or sets the frame delay fraction denominator
+ ///
+ public short DelayDenominator { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area disposal to be done after rendering this frame
+ ///
+ public APngDisposeOperation DisposeOperation { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area rendering for this frame
+ ///
+ public APngBlendOperation BlendOperation { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chunk to create an instance from.
+ 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;
+ }
+
+ ///
+ public IDeepCloneable DeepClone() => new APngFrameMetadata(this);
+}
diff --git a/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs
new file mode 100644
index 0000000000..ca8268cd5d
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs
@@ -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(
+ int NumberFrames,
+ int NumberPlays)
+{
+ public const int Size = 8;
+
+ ///
+ /// Gets the number of frames
+ ///
+ public int NumberFrames { get; } = NumberFrames;
+
+ ///
+ /// Gets the number of times to loop this APNG. 0 indicates infinite looping.
+ ///
+ public int NumberPlays { get; } = NumberPlays;
+
+ ///
+ /// Writes the acTL to the given buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
+ BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
+ }
+
+ ///
+ /// Parses the APngAnimationControl from the given data buffer.
+ ///
+ /// The data to parse.
+ /// The parsed acTL.
+ public static APngAnimationControl Parse(ReadOnlySpan data)
+ => new(
+ NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
+ NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
+}
diff --git a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
new file mode 100644
index 0000000000..e239bd8e2e
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
@@ -0,0 +1,152 @@
+// 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(
+ 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;
+ }
+
+ ///
+ /// Gets the sequence number of the animation chunk, starting from 0
+ ///
+ public int SequenceNumber { get; }
+
+ ///
+ /// Gets the width of the following frame
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height of the following frame
+ ///
+ public int Height { get; }
+
+ ///
+ /// Gets the X position at which to render the following frame
+ ///
+ public int XOffset { get; }
+
+ ///
+ /// Gets the Y position at which to render the following frame
+ ///
+ public int YOffset { get; }
+
+ ///
+ /// Gets the frame delay fraction numerator
+ ///
+ public short DelayNumber { get; }
+
+ ///
+ /// Gets the frame delay fraction denominator
+ ///
+ public short DelayDenominator { get; }
+
+ ///
+ /// Gets the type of frame area disposal to be done after rendering this frame
+ ///
+ public APngDisposeOperation DisposeOperation { get; }
+
+ ///
+ /// Gets the type of frame area rendering for this frame
+ ///
+ public APngBlendOperation BlendOperation { get; }
+
+ ///
+ /// Validates the APng fcTL.
+ ///
+ ///
+ /// Thrown if the image does pass validation.
+ ///
+ 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}'.");
+ }
+
+ if (this.YOffset + this.Height > hdr.Height)
+ {
+ throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'.");
+ }
+ }
+
+ ///
+ /// Writes the fcTL to the given buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteTo(Span 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.WriteInt32BigEndian(buffer[20..22], this.DelayNumber);
+ BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator);
+
+ buffer[24] = (byte)this.DisposeOperation;
+ buffer[25] = (byte)this.BlendOperation;
+ }
+
+ ///
+ /// Parses the APngFrameControl from the given data buffer.
+ ///
+ /// The data to parse.
+ /// The parsed fcTL.
+ public static APngFrameControl Parse(ReadOnlySpan 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]);
+}
diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
similarity index 98%
rename from src/ImageSharp/Formats/Png/PngHeader.cs
rename to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
index 06fec86f30..77fb706f60 100644
--- a/src/ImageSharp/Formats/Png/PngHeader.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
@@ -4,7 +4,7 @@
using System.Buffers.Binary;
-namespace SixLabors.ImageSharp.Formats.Png;
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
///
/// Represents the png header chunk.
diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
similarity index 89%
rename from src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs
rename to src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
index 34d53f00eb..7847882484 100644
--- a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
@@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
///
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
///
-internal readonly struct PhysicalChunkData
+internal readonly struct PngPhysical
{
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;
@@ -44,13 +44,13 @@ internal readonly struct PhysicalChunkData
///
/// The data buffer.
/// The parsed PhysicalChunkData.
- public static PhysicalChunkData Parse(ReadOnlySpan data)
+ public static PngPhysical Parse(ReadOnlySpan 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);
}
///
@@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData
///
/// The metadata.
/// The constructed PngPhysicalChunkData instance.
- public static PhysicalChunkData FromMetadata(ImageMetadata meta)
+ public static PngPhysical FromMetadata(ImageMetadata meta)
{
byte unitSpecifier = 0;
uint x;
@@ -92,7 +92,7 @@ internal readonly struct PhysicalChunkData
break;
}
- return new PhysicalChunkData(x, y, unitSpecifier);
+ return new PngPhysical(x, y, unitSpecifier);
}
///
diff --git a/src/ImageSharp/Formats/Png/PngTextData.cs b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
similarity index 99%
rename from src/ImageSharp/Formats/Png/PngTextData.cs
rename to src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
index 8ef4f1821d..077eb46082 100644
--- a/src/ImageSharp/Formats/Png/PngTextData.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
@@ -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;
///
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks.
diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
index e05bd5f844..0ae180e08d 100644
--- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
@@ -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;
@@ -14,7 +15,23 @@ public static partial class MetadataExtensions
///
/// Gets the png format specific metadata for the image.
///
- /// The metadata this method extends.
+ /// The metadata this method extends.
/// The .
- public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance);
+ public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
+
+ ///
+ /// Gets the aPng format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static APngFrameMetadata GetAPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
+
+ ///
+ /// Gets the aPng format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The metadata.
+ /// The .
+ public static bool TryGetAPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out APngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
+
}
diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs
index b514011eb3..e5fa5fbb72 100644
--- a/src/ImageSharp/Formats/Png/PngChunk.cs
+++ b/src/ImageSharp/Formats/Png/PngChunk.cs
@@ -42,7 +42,8 @@ internal readonly struct PngChunk
/// Gets a value indicating whether the given chunk is critical to decoding
///
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;
}
diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs
index f47c2e7f86..2c835bf8ca 100644
--- a/src/ImageSharp/Formats/Png/PngChunkType.cs
+++ b/src/ImageSharp/Formats/Png/PngChunkType.cs
@@ -8,16 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
internal enum PngChunkType : uint
{
+ ///
+ ///
+ /// acTL
+ AnimationControl = 0x6163544cU,
+
+ ///
+ ///
+ /// fcTL
+ FrameControl = 0x6663544cU,
+
+ ///
+ ///
+ /// fdAT
+ FrameData = 0x66644154U,
+
///
/// The IDAT chunk contains the actual image data. The image can contains more
/// than one chunk of this type. All chunks together are the whole image.
///
+ /// IDAT
Data = 0x49444154U,
///
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
///
+ /// IEND
End = 0x49454E44U,
///
@@ -25,34 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
///
+ /// IHDR
Header = 0x49484452U,
///
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
///
+ /// PLTE
Palette = 0x504C5445U,
///
/// The eXIf data chunk which contains the Exif profile.
///
+ /// eXIF
Exif = 0x65584966U,
///
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
///
+ /// gAMA
Gamma = 0x67414D41U,
///
- /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
+ /// This chunk specifies the intended pixel size or aspect ratio for display of the image.
///
+ /// pHYs
Physical = 0x70485973U,
///
/// Textual information that the encoder wishes to record with the image can be stored in
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
///
+ /// tEXT
Text = 0x74455874U,
///
@@ -60,70 +83,82 @@ internal enum PngChunkType : uint
/// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string.
///
+ /// zTXt
CompressedText = 0x7A545874U,
///
- /// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
+ /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
/// and the actual text string, which can be compressed or uncompressed.
///
+ /// iTXt
InternationalText = 0x69545874U,
///
- /// The tRNS chunk specifies that the image uses simple transparency:
+ /// This chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images).
///
+ /// tRNS
Transparency = 0x74524E53U,
///
- /// The tIME chunk gives the time of the last image modification (not the time of initial image creation).
+ /// This chunk gives the time of the last image modification (not the time of initial image creation).
///
+ /// tIME
Time = 0x74494d45,
///
- /// The bKGD chunk specifies a default background colour to present the image against.
+ /// This chunk specifies a default background colour to present the image against.
/// If there is any other preferred background, either user-specified or part of a larger page (as in a browser),
/// the bKGD chunk should be ignored.
///
+ /// bKGD
Background = 0x624b4744,
///
- /// The iCCP chunk contains a embedded color profile. If the iCCP chunk is present,
+ /// This chunk contains a embedded color profile. If the iCCP chunk is present,
/// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
///
+ /// iCCP
EmbeddedColorProfile = 0x69434350,
///
- /// The sBIT chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
+ /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
/// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
///
+ /// sBIT
SignificantBits = 0x73424954,
///
- /// If the sRGB chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
+ /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
/// using the specified rendering intent defined by the International Color Consortium.
///
+ /// sRGB
StandardRgbColourSpace = 0x73524742,
///
- /// The hIST chunk gives the approximate usage frequency of each colour in the palette.
+ /// This chunk gives the approximate usage frequency of each colour in the palette.
///
+ /// hIST
Histogram = 0x68495354,
///
- /// The sPLT chunk contains the suggested palette.
+ /// This chunk contains the suggested palette.
///
+ /// sPLT
SuggestedPalette = 0x73504c54,
///
- /// The cHRM chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
+ /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
/// green, and blue display primaries used in the image, and the referenced white point.
///
+ /// cHRM
Chroma = 0x6348524d,
///
/// Malformed chunk named CgBI produced by apple, which is not conform to the specification.
/// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410
///
+ /// CgBI
ProprietaryApple = 0x43674249
}
diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs
index b76c73b9f2..7877f84bd8 100644
--- a/src/ImageSharp/Formats/Png/PngConstants.cs
+++ b/src/ImageSharp/Formats/Png/PngConstants.cs
@@ -28,12 +28,12 @@ internal static class PngConstants
///
/// The list of mimetypes that equate to a Png.
///
- public static readonly IEnumerable MimeTypes = new[] { "image/png" };
+ public static readonly IEnumerable MimeTypes = new[] { "image/png", "image/apng" };
///
/// The list of file extensions that equate to a Png.
///
- public static readonly IEnumerable FileExtensions = new[] { "png" };
+ public static readonly IEnumerable FileExtensions = new[] { "png", "apng" };
///
/// The header bytes as a big-endian coded ulong.
@@ -43,7 +43,7 @@ internal static class PngConstants
///
/// The dictionary of available color types.
///
- public static readonly Dictionary ColorTypes = new Dictionary
+ public static readonly Dictionary ColorTypes = new()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
@@ -80,24 +80,5 @@ internal static class PngConstants
///
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
///
- public static ReadOnlySpan XmpKeyword => new byte[]
- {
- (byte)'X',
- (byte)'M',
- (byte)'L',
- (byte)':',
- (byte)'c',
- (byte)'o',
- (byte)'m',
- (byte)'.',
- (byte)'a',
- (byte)'d',
- (byte)'o',
- (byte)'b',
- (byte)'e',
- (byte)'.',
- (byte)'x',
- (byte)'m',
- (byte)'p'
- };
+ public static ReadOnlySpan XmpKeyword => "XML:com.adobe.xmp"u8;
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index d1d29dca6b..fa94e6925c 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -1,9 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using System.Buffers;
using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Compression;
using System.Runtime.CompilerServices;
@@ -28,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
internal sealed class PngDecoderCore : IImageDecoderInternals
{
+ ///
+ /// Indicate whether the file is a simple PNG.
+ ///
+ private bool isSimplePng;
+
///
/// The general decoder options.
///
@@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
/// The stream to decode from.
///
- private BufferedReadStream currentStream;
+ private BufferedReadStream currentStream = null!;
///
/// The png header.
///
private PngHeader header;
+ ///
+ /// The png animation control.
+ ///
+ private APngAnimationControl? animationControl;
+
///
/// The number of bytes per pixel.
///
@@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
/// The palette containing color information for indexed png's.
///
- private byte[] palette;
+ private byte[] palette = null!;
///
/// The palette containing alpha channel color information for indexed png's.
///
- private byte[] paletteAlpha;
+ private byte[] paletteAlpha = null!;
///
/// Previous scanline processed.
///
- private IMemoryOwner previousScanline;
+ private IMemoryOwner previousScanline = null!;
///
/// The current scanline that is being processed.
///
- private IMemoryOwner scanline;
-
- ///
- /// The index of the current scanline being processed.
- ///
- private int currentRow = Adam7.FirstRow[0];
-
- ///
- /// The current number of bytes read in the current scanline.
- ///
- private int currentRowBytesRead;
+ private IMemoryOwner scanline = null!;
///
/// Gets or sets the png color type.
@@ -148,7 +148,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
this.currentStream.Skip(8);
- Image image = null;
+ Image? image = null;
+ APngFrameControl? lastFrameControl = null;
+ ImageFrame? currentFrame = null;
Span buffer = stackalloc byte[20];
try
@@ -160,22 +162,84 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
switch (chunk.Type)
{
case PngChunkType.Header:
+ if (!Equals(this.header, default(PngHeader)))
+ {
+ PngThrowHelper.ThrowInvalidHeader();
+ }
+
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.AnimationControl:
+ if (this.isSimplePng || this.animationControl is not null)
+ {
+ PngThrowHelper.ThrowInvalidAnimationControl();
+ }
+
+ this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
+ break;
case PngChunkType.Physical:
ReadPhysicalChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Gamma:
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
- case PngChunkType.Data:
+ case PngChunkType.FrameControl:
+ if (this.isSimplePng)
+ {
+ continue;
+ }
+
+ currentFrame = null;
+ lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
if (image is null)
{
- this.InitializeImage(metadata, out image);
+ PngThrowHelper.ThrowMissingDefaultData();
}
- this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken);
+ if (lastFrameControl is null)
+ {
+ PngThrowHelper.ThrowMissingFrameControl();
+ }
+
+ if (currentFrame is null)
+ {
+ this.InitializeFrame(lastFrameControl.Value, image, out currentFrame);
+ }
+
+ this.currentStream.Position += 4;
+ this.ReadScanlines(
+ chunk.Length - 4,
+ currentFrame,
+ pngMetadata,
+ () =>
+ {
+ int length = this.ReadNextDataChunk();
+ if (this.ReadNextDataChunk() is 0)
+ {
+ return length;
+ }
+
+ this.currentStream.Position += 4; // Skip sequence number
+ return length - 4;
+ },
+ cancellationToken);
+ lastFrameControl = null;
+ break;
+ case PngChunkType.Data:
+ if (this.animationControl is null)
+ {
+ this.isSimplePng = true;
+ }
+
+ if (image is null)
+ {
+ this.InitializeImage(metadata, lastFrameControl, out image);
+ }
+ this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, cancellationToken);
+ lastFrameControl = null;
break;
case PngChunkType.Palette:
byte[] pal = new byte[chunk.Length];
@@ -249,6 +313,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
+ APngFrameControl? lastFrameControl = null;
Span buffer = stackalloc byte[20];
this.currentStream.Skip(8);
@@ -264,6 +329,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngChunkType.Header:
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.AnimationControl:
+ if (this.isSimplePng || this.animationControl is not null)
+ {
+ PngThrowHelper.ThrowInvalidAnimationControl();
+ }
+
+ this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
+ break;
case PngChunkType.Physical:
if (this.colorMetadataOnly)
{
@@ -282,7 +355,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.FrameControl:
+ if (this.isSimplePng)
+ {
+ continue;
+ }
+
+ lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
+ if (this.colorMetadataOnly)
+ {
+ goto EOF;
+ }
+
+ if (lastFrameControl is null)
+ {
+ PngThrowHelper.ThrowMissingFrameControl();
+ }
+
+ // Skip sequence number
+ this.currentStream.Skip(4);
+ this.SkipChunkDataAndCrc(chunk);
+ break;
case PngChunkType.Data:
+ if (this.animationControl is null)
+ {
+ this.isSimplePng = true;
+ }
// Spec says tRNS must be before IDAT so safe to exit.
if (this.colorMetadataOnly)
@@ -365,9 +465,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
}
EOF:
- if (this.header.Width == 0 && this.header.Height == 0)
+ if (this.header is { Width: 0, Height: 0 })
{
- PngThrowHelper.ThrowNoHeader();
+ PngThrowHelper.ThrowInvalidHeader();
}
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
@@ -398,7 +498,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// The number of bits per value.
/// The new array.
/// The resulting array.
- private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IMemoryOwner buffer)
+ private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner? buffer)
{
if (bits >= 8)
{
@@ -433,7 +533,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// The data containing physical data.
private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan data)
{
- PhysicalChunkData physicalChunk = PhysicalChunkData.Parse(data);
+ PngPhysical physicalChunk = PngPhysical.Parse(data);
metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue
? PixelResolutionUnit.AspectRatio
@@ -466,8 +566,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
/// The type the pixels will be
/// The metadata information for the image
+ /// The frame control information for the frame
/// The image that we will populate
- private void InitializeImage(ImageMetadata metadata, out Image image)
+ private void InitializeImage(ImageMetadata metadata, APngFrameControl? frameControl, out Image image)
where TPixel : unmanaged, IPixel
{
image = Image.CreateUninitialized(
@@ -476,6 +577,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header.Height,
metadata);
+ if (frameControl is { } control)
+ {
+ APngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAPngFrameMetadata();
+ frameMetadata.FromChunk(control);
+ }
+
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
@@ -490,6 +597,27 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
}
+ ///
+ /// Initializes the image and various buffers needed for processing
+ ///
+ /// The type the pixels will be
+ /// The frame control information for the frame
+ /// The image that we will populate
+ private void InitializeFrame(APngFrameControl frameControl, Image image, out ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ frame = image.Frames.CreateFrame();
+
+ APngFrameMetadata frameMetadata = frame.Metadata.GetAPngFrameMetadata();
+
+ frameMetadata.FromChunk(frameControl);
+
+ this.previousScanline?.Dispose();
+ this.scanline?.Dispose();
+ this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
+ this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
+ }
+
///
/// Calculates the correct number of bits per pixel for the given color type.
///
@@ -553,18 +681,19 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Reads the scanlines within the image.
///
/// The pixel format.
- /// The png chunk containing the compressed scanline data.
+ /// The length of the chunk that containing the compressed scanline data.
/// The pixel data.
/// The png metadata
+ /// A delegate to get more data from the inner stream for .
/// The cancellation token.
- private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk);
- deframeStream.AllocateNewBytes(chunk.Length, true);
- DeflateStream dataStream = deframeStream.CompressedStream;
+ using ZlibInflateStream deframeStream = new(this.currentStream, getData);
+ deframeStream.AllocateNewBytes(chunkLength, true);
+ DeflateStream dataStream = deframeStream.CompressedStream!;
- if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
+ if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken);
}
@@ -585,22 +714,25 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- while (this.currentRow < this.header.Height)
+ int currentRow = Adam7.FirstRow[0];
+ int currentRowBytesRead = 0;
+ int height = image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height;
+ while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
Span scanlineSpan = this.scanline.GetSpan();
- while (this.currentRowBytesRead < this.bytesPerScanline)
+ while (currentRowBytesRead < this.bytesPerScanline)
{
- int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
+ int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, this.bytesPerScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
}
- this.currentRowBytesRead += bytesRead;
+ currentRowBytesRead += bytesRead;
}
- this.currentRowBytesRead = 0;
+ currentRowBytesRead = 0;
switch ((FilterType)scanlineSpan[0])
{
@@ -628,10 +760,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata);
+ this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata);
this.SwapScanlineBuffers();
- this.currentRow++;
+ ++currentRow;
}
}
@@ -647,8 +779,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ int currentRow = Adam7.FirstRow[0];
+ int currentRowBytesRead = 0;
int pass = 0;
int width = this.header.Width;
+ int height = this.header.Height;
+ if (image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata))
+ {
+ width = frameMetadata.Width;
+ height = frameMetadata.Height;
+ }
+
Buffer2D imageBuffer = image.PixelBuffer;
while (true)
{
@@ -656,7 +797,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (numColumns == 0)
{
- pass++;
+ ++pass;
// This pass contains no data; skip to next pass
continue;
@@ -664,21 +805,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
- while (this.currentRow < this.header.Height)
+ while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
- while (this.currentRowBytesRead < bytesPerInterlaceScanline)
+ while (currentRowBytesRead < bytesPerInterlaceScanline)
{
- int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead);
+ int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
}
- this.currentRowBytesRead += bytesRead;
+ currentRowBytesRead += bytesRead;
}
- this.currentRowBytesRead = 0;
+ currentRowBytesRead = 0;
Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline);
Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline);
@@ -709,12 +850,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow);
+ Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
this.SwapScanlineBuffers();
- this.currentRow += Adam7.RowIncrement[pass];
+ currentRow += Adam7.RowIncrement[pass];
}
pass++;
@@ -722,7 +863,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (pass < 7)
{
- this.currentRow = Adam7.FirstRow[pass];
+ currentRow = Adam7.FirstRow[pass];
}
else
{
@@ -736,19 +877,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Processes the de-filtered scanline filling the image pixel data
///
/// The pixel format.
+ /// The index of the current scanline being processed.
/// The de-filtered scanline
/// The image
/// The png metadata.
- private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata)
+ private void ProcessDefilteredScanline(int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel
{
- Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow);
+ Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
// Trim the first marker byte from the buffer
ReadOnlySpan trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- IMemoryOwner buffer = null;
+ IMemoryOwner? buffer = null;
try
{
ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
@@ -840,7 +982,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- IMemoryOwner buffer = null;
+ IMemoryOwner? buffer = null;
try
{
ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
@@ -975,6 +1117,31 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
}
}
+ ///
+ /// Reads a animation control chunk from the data.
+ ///
+ /// The png metadata.
+ /// The containing data.
+ private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data)
+ {
+ this.animationControl = APngAnimationControl.Parse(data);
+
+ pngMetadata.NumberPlays = this.animationControl.NumberPlays;
+ }
+
+ ///
+ /// Reads a header chunk from the data.
+ ///
+ /// The containing data.
+ private APngFrameControl ReadFrameControlChunk(ReadOnlySpan data)
+ {
+ APngFrameControl fcTL = APngFrameControl.Parse(data);
+
+ fcTL.Validate(this.header);
+
+ return fcTL;
+ }
+
///
/// Reads a header chunk from the data.
///
@@ -1062,7 +1229,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan compressedData = data[(zeroIndex + 2)..];
- if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed)
+ if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed)
&& !TryReadTextChunkMetadata(baseMetadata, name, uncompressed))
{
metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty));
@@ -1355,7 +1522,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
ReadOnlySpan compressedData = data[dataStartIdx..];
- if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed))
+ if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed))
{
pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
}
@@ -1378,7 +1545,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// The string encoding to use.
/// The uncompressed value.
/// The .
- private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value)
+ private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, [NotNullWhen(true)] out string? value)
{
if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
{
@@ -1407,7 +1574,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (this.TryReadChunk(buffer, out PngChunk chunk))
{
- if (chunk.Type == PngChunkType.Data)
+ if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData)
{
chunk.Data?.Dispose();
return chunk.Length;
@@ -1461,7 +1628,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// If we're reading color metadata only we're only interested in the IHDR and tRNS chunks.
// We can skip all other chunk data in the stream for better performance.
- if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency)
+ if (this.colorMetadataOnly && type is not PngChunkType.Header and not PngChunkType.Transparency)
{
chunk = new PngChunk(length, type);
@@ -1476,9 +1643,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.ValidateChunk(chunk, buffer);
- // Restore the stream position for IDAT chunks, because it will be decoded later and
+ // Restore the stream position for IDAT and fdAT chunks, because it will be decoded later and
// was only read to verifying the CRC is correct.
- if (type == PngChunkType.Data)
+ if (type is PngChunkType.Data or PngChunkType.FrameData)
{
this.currentStream.Position = pos;
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 175a9f777d..8fcd1721d3 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
+using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory;
@@ -647,9 +648,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return;
}
- PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
+ PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
- this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size);
+ this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size);
}
///
diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs
index 2d1f2dcc7d..292f087f27 100644
--- a/src/ImageSharp/Formats/Png/PngFormat.cs
+++ b/src/ImageSharp/Formats/Png/PngFormat.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
/// Registers the image encoders, decoders and mime type detectors for the png format.
///
-public sealed class PngFormat : IImageFormat
+public sealed class PngFormat : IImageFormat
{
private PngFormat()
{
@@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat
///
public PngMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public APngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 9ff3905fe1..9f874d5c9a 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Gif;
+using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -32,6 +34,7 @@ public class PngMetadata : IDeepCloneable
this.TransparentL16 = other.TransparentL16;
this.TransparentRgb24 = other.TransparentRgb24;
this.TransparentRgb48 = other.TransparentRgb48;
+ this.NumberPlays = other.NumberPlays;
for (int i = 0; i < other.TextData.Count; i++)
{
@@ -95,6 +98,11 @@ public class PngMetadata : IDeepCloneable
///
public IList TextData { get; set; } = new List();
+ ///
+ /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
+ ///
+ public int NumberPlays { get; set; }
+
///
public IDeepCloneable DeepClone() => new PngMetadata(this);
}
diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
index 04a23308cc..caba887921 100644
--- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
+++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
@@ -4,6 +4,7 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
index 67da78e45b..78c243eeef 100644
--- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs
+++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
@@ -12,13 +12,22 @@ internal static class PngThrowHelper
=> throw new InvalidImageContentException(errorMessage, innerException);
[DoesNotReturn]
- public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk");
+ public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks.");
[DoesNotReturn]
- public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk");
+ public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk.");
[DoesNotReturn]
- public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk");
+ public static void ThrowMissingDefaultData() => throw new InvalidImageContentException("APNG Image does not contain a default data chunk.");
+
+ [DoesNotReturn]
+ public static void ThrowInvalidAnimationControl() => throw new InvalidImageContentException("APNG Image must contain a acTL chunk and it must be located before any IDAT and fdAT chunks.");
+
+ [DoesNotReturn]
+ public static void ThrowMissingFrameControl() => throw new InvalidImageContentException("One of APNG Image's frames do not have a frame control chunk.");
+
+ [DoesNotReturn]
+ public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk.");
[DoesNotReturn]
public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data.");
@@ -30,7 +39,7 @@ internal static class PngThrowHelper
public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!");
[DoesNotReturn]
- public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type");
+ public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type.");
[DoesNotReturn]
public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type.");
diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj
index 75d4b173c8..57608a9090 100644
--- a/src/ImageSharp/ImageSharp.csproj
+++ b/src/ImageSharp/ImageSharp.csproj
@@ -13,6 +13,7 @@
Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore
A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET
Debug;Release
+ preview
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index d7a353665a..ff81401f56 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
@@ -132,6 +134,34 @@ public class PngMetadataTests
VerifyExifDataIsPresent(exif);
}
+ [Theory]
+ [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)]
+ public void Decode_ReadsExifData2(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ DecoderOptions options = new()
+ {
+ SkipMetadata = false
+ };
+
+ using Image image = provider.GetImage(PngDecoder.Instance, options);
+ TPixel pixel = image.Frames.RootFrame[5, 5];
+ TPixel pixel2 = image.Frames[1][5, 5];
+ }
+
+ [Theory]
+ [WithFile(@"Png\pl.png", PixelTypes.Rgba32)]
+ public void Decode_ReadsExifData3(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ DecoderOptions options = new()
+ {
+ SkipMetadata = false
+ };
+
+ using Image image = provider.GetImage(PngDecoder.Instance, options);
+ }
+
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
index 04341a2419..96b5b620b8 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
@@ -1,7 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Formats.Gif;
+using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.Png;