Browse Source

Implement APNG decoder

pull/2511/head
Poker 3 years ago
parent
commit
45f6f5b154
No known key found for this signature in database GPG Key ID: 720AFAD63099D9CB
  1. 8
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  2. 20
      src/ImageSharp/Formats/Png/APngBlendOperation.cs
  3. 25
      src/ImageSharp/Formats/Png/APngDisposeOperation.cs
  4. 94
      src/ImageSharp/Formats/Png/APngFrameMetadata.cs
  5. 43
      src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs
  6. 152
      src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
  7. 2
      src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
  8. 14
      src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
  9. 2
      src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
  10. 21
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  11. 7
      src/ImageSharp/Formats/Png/PngChunk.cs
  12. 57
      src/ImageSharp/Formats/Png/PngChunkType.cs
  13. 27
      src/ImageSharp/Formats/Png/PngConstants.cs
  14. 283
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  15. 5
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  16. 5
      src/ImageSharp/Formats/Png/PngFormat.cs
  17. 8
      src/ImageSharp/Formats/Png/PngMetadata.cs
  18. 1
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  19. 17
      src/ImageSharp/Formats/Png/PngThrowHelper.cs
  20. 1
      src/ImageSharp/ImageSharp.csproj
  21. 30
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  22. 3
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

8
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream
/// <inheritdoc/> /// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) 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. // 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(); this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0) if (this.currentDataRemaining is 0)
{ {
return 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. // Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0; int bytesRead = 0;
offset += totalBytesRead; offset += totalBytesRead;
while (this.currentDataRemaining == 0 && totalBytesRead < count) while (this.currentDataRemaining is 0 && totalBytesRead < count)
{ {
this.currentDataRemaining = this.getData(); this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0) if (this.currentDataRemaining is 0)
{ {
return totalBytesRead; return totalBytesRead;
} }

20
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;
/// <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
{
/// <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
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;
/// <summary>
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
/// </summary>
public enum APngDisposeOperation
{
/// <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
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;
/// <summary>
/// Provides APng specific metadata information for the image frame.
/// </summary>
public class APngFrameMetadata : IDeepCloneable
{
/// <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
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;
/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; } = NumberFrames;
/// <summary>
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; } = NumberPlays;
/// <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]));
}

152
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;
}
/// <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}'.");
}
if (this.YOffset + this.Height > hdr.Height)
{
throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'.");
}
}
/// <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.WriteInt32BigEndian(buffer[20..22], this.DelayNumber);
BinaryPrimitives.WriteInt32BigEndian(buffer[12..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]);
}

2
src/ImageSharp/Formats/Png/PngHeader.cs → src/ImageSharp/Formats/Png/Chunks/PngHeader.cs

@ -4,7 +4,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// Represents the png header chunk. /// Represents the png header chunk.

14
src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs → 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. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary> /// </summary>
internal readonly struct PhysicalChunkData internal readonly struct PngPhysical
{ {
public const int Size = 9; 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.XAxisPixelsPerUnit = x;
this.YAxisPixelsPerUnit = y; this.YAxisPixelsPerUnit = y;
@ -44,13 +44,13 @@ internal readonly struct PhysicalChunkData
/// </summary> /// </summary>
/// <param name="data">The data buffer.</param> /// <param name="data">The data buffer.</param>
/// <returns>The parsed PhysicalChunkData.</returns> /// <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 hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]);
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8]; byte unit = data[8];
return new PhysicalChunkData(hResolution, vResolution, unit); return new PngPhysical(hResolution, vResolution, unit);
} }
/// <summary> /// <summary>
@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData
/// </summary> /// </summary>
/// <param name="meta">The metadata.</param> /// <param name="meta">The metadata.</param>
/// <returns>The constructed PngPhysicalChunkData instance.</returns> /// <returns>The constructed PngPhysicalChunkData instance.</returns>
public static PhysicalChunkData FromMetadata(ImageMetadata meta) public static PngPhysical FromMetadata(ImageMetadata meta)
{ {
byte unitSpecifier = 0; byte unitSpecifier = 0;
uint x; uint x;
@ -92,7 +92,7 @@ internal readonly struct PhysicalChunkData
break; break;
} }
return new PhysicalChunkData(x, y, unitSpecifier); return new PngPhysical(x, y, unitSpecifier);
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Png/PngTextData.cs → src/ImageSharp/Formats/Png/Chunks/PngTextData.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks. /// Stores text data contained in the iTXt, tEXt, and zTXt chunks.

21
src/ImageSharp/Formats/Png/MetadataExtensions.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -14,7 +15,23 @@ public static partial class MetadataExtensions
/// <summary> /// <summary>
/// Gets the png format specific metadata for the image. /// Gets the png format specific metadata for the image.
/// </summary> /// </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> /// <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="APngFrameMetadata"/>.</returns>
public static APngFrameMetadata GetAPngFrameMetadata(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="APngFrameMetadata"/>.</returns>
public static bool TryGetAPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out APngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
} }

7
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 /// Gets a value indicating whether the given chunk is critical to decoding
/// </summary> /// </summary>
public bool IsCritical => public bool IsCritical =>
this.Type == PngChunkType.Header || this.Type is PngChunkType.Header or
this.Type == PngChunkType.Palette || PngChunkType.Palette or
this.Type == PngChunkType.Data; PngChunkType.Data or
PngChunkType.FrameData;
} }

57
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -8,16 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary> /// </summary>
internal enum PngChunkType : uint internal enum PngChunkType : uint
{ {
/// <summary>
/// </summary>
/// <remarks>acTL</remarks>
AnimationControl = 0x6163544cU,
/// <summary>
/// </summary>
/// <remarks>fcTL</remarks>
FrameControl = 0x6663544cU,
/// <summary>
/// </summary>
/// <remarks>fdAT</remarks>
FrameData = 0x66644154U,
/// <summary> /// <summary>
/// The IDAT chunk contains the actual image data. The image can contains more /// 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. /// than one chunk of this type. All chunks together are the whole image.
/// </summary> /// </summary>
/// <remarks>IDAT</remarks>
Data = 0x49444154U, Data = 0x49444154U,
/// <summary> /// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream. /// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty. /// The chunk's data field is empty.
/// </summary> /// </summary>
/// <remarks>IEND</remarks>
End = 0x49454E44U, End = 0x49454E44U,
/// <summary> /// <summary>
@ -25,34 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or /// common information like the width and the height of the image or
/// the used compression method. /// the used compression method.
/// </summary> /// </summary>
/// <remarks>IHDR</remarks>
Header = 0x49484452U, Header = 0x49484452U,
/// <summary> /// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format. /// series in the RGB format.
/// </summary> /// </summary>
/// <remarks>PLTE</remarks>
Palette = 0x504C5445U, Palette = 0x504C5445U,
/// <summary> /// <summary>
/// The eXIf data chunk which contains the Exif profile. /// The eXIf data chunk which contains the Exif profile.
/// </summary> /// </summary>
/// <remarks>eXIF</remarks>
Exif = 0x65584966U, Exif = 0x65584966U,
/// <summary> /// <summary>
/// This chunk specifies the relationship between the image samples and the desired /// This chunk specifies the relationship between the image samples and the desired
/// display output intensity. /// display output intensity.
/// </summary> /// </summary>
/// <remarks>gAMA</remarks>
Gamma = 0x67414D41U, Gamma = 0x67414D41U,
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks>pHYs</remarks>
Physical = 0x70485973U, Physical = 0x70485973U,
/// <summary> /// <summary>
/// Textual information that the encoder wishes to record with the image can be stored in /// 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 chunks. Each tEXt chunk contains a keyword and a text string.
/// </summary> /// </summary>
/// <remarks>tEXT</remarks>
Text = 0x74455874U, Text = 0x74455874U,
/// <summary> /// <summary>
@ -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 /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string. /// a compressed text string.
/// </summary> /// </summary>
/// <remarks>zTXt</remarks>
CompressedText = 0x7A545874U, CompressedText = 0x7A545874U,
/// <summary> /// <summary>
/// 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. /// and the actual text string, which can be compressed or uncompressed.
/// </summary> /// </summary>
/// <remarks>iTXt</remarks>
InternationalText = 0x69545874U, InternationalText = 0x69545874U,
/// <summary> /// <summary>
/// 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) /// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images). /// or a single transparent color (for grayscale and true color images).
/// </summary> /// </summary>
/// <remarks>tRNS</remarks>
Transparency = 0x74524E53U, Transparency = 0x74524E53U,
/// <summary> /// <summary>
/// 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).
/// </summary> /// </summary>
/// <remarks>tIME</remarks>
Time = 0x74494d45, Time = 0x74494d45,
/// <summary> /// <summary>
/// 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), /// 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. /// the bKGD chunk should be ignored.
/// </summary> /// </summary>
/// <remarks>bKGD</remarks>
Background = 0x624b4744, Background = 0x624b4744,
/// <summary> /// <summary>
/// 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. /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>iCCP</remarks>
EmbeddedColorProfile = 0x69434350, EmbeddedColorProfile = 0x69434350,
/// <summary> /// <summary>
/// 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. /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
/// </summary> /// </summary>
/// <remarks>sBIT</remarks>
SignificantBits = 0x73424954, SignificantBits = 0x73424954,
/// <summary> /// <summary>
/// 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. /// using the specified rendering intent defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>sRGB</remarks>
StandardRgbColourSpace = 0x73524742, StandardRgbColourSpace = 0x73524742,
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks>hIST</remarks>
Histogram = 0x68495354, Histogram = 0x68495354,
/// <summary> /// <summary>
/// The sPLT chunk contains the suggested palette. /// This chunk contains the suggested palette.
/// </summary> /// </summary>
/// <remarks>sPLT</remarks>
SuggestedPalette = 0x73504c54, SuggestedPalette = 0x73504c54,
/// <summary> /// <summary>
/// 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. /// green, and blue display primaries used in the image, and the referenced white point.
/// </summary> /// </summary>
/// <remarks>cHRM</remarks>
Chroma = 0x6348524d, Chroma = 0x6348524d,
/// <summary> /// <summary>
/// Malformed chunk named CgBI produced by apple, which is not conform to the specification. /// 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 /// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410
/// </summary> /// </summary>
/// <remarks>CgBI</remarks>
ProprietaryApple = 0x43674249 ProprietaryApple = 0x43674249
} }

27
src/ImageSharp/Formats/Png/PngConstants.cs

@ -28,12 +28,12 @@ internal static class PngConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a Png. /// The list of mimetypes that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png" }; public static readonly IEnumerable<string> MimeTypes = new[] { "image/png", "image/apng" };
/// <summary> /// <summary>
/// The list of file extensions that equate to a Png. /// The list of file extensions that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png" }; public static readonly IEnumerable<string> FileExtensions = new[] { "png", "apng" };
/// <summary> /// <summary>
/// The header bytes as a big-endian coded ulong. /// The header bytes as a big-endian coded ulong.
@ -43,7 +43,7 @@ internal static class PngConstants
/// <summary> /// <summary>
/// The dictionary of available color types. /// The dictionary of available color types.
/// </summary> /// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]> public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new()
{ {
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 }, [PngColorType.Rgb] = new byte[] { 8, 16 },
@ -80,24 +80,5 @@ internal static class PngConstants
/// <summary> /// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new byte[] public static ReadOnlySpan<byte> XmpKeyword => "XML:com.adobe.xmp"u8;
{
(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'
};
} }

283
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -1,9 +1,9 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -28,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary> /// </summary>
internal sealed class PngDecoderCore : IImageDecoderInternals internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
/// <summary>
/// Indicate whether the file is a simple PNG.
/// </summary>
private bool isSimplePng;
/// <summary> /// <summary>
/// The general decoder options. /// The general decoder options.
/// </summary> /// </summary>
@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The stream to decode from. /// The stream to decode from.
/// </summary> /// </summary>
private BufferedReadStream currentStream; private BufferedReadStream currentStream = null!;
/// <summary> /// <summary>
/// The png header. /// The png header.
/// </summary> /// </summary>
private PngHeader header; private PngHeader header;
/// <summary>
/// The png animation control.
/// </summary>
private APngAnimationControl? animationControl;
/// <summary> /// <summary>
/// The number of bytes per pixel. /// The number of bytes per pixel.
/// </summary> /// </summary>
@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The palette containing color information for indexed png's. /// The palette containing color information for indexed png's.
/// </summary> /// </summary>
private byte[] palette; private byte[] palette = null!;
/// <summary> /// <summary>
/// The palette containing alpha channel color information for indexed png's. /// The palette containing alpha channel color information for indexed png's.
/// </summary> /// </summary>
private byte[] paletteAlpha; private byte[] paletteAlpha = null!;
/// <summary> /// <summary>
/// Previous scanline processed. /// Previous scanline processed.
/// </summary> /// </summary>
private IMemoryOwner<byte> previousScanline; private IMemoryOwner<byte> previousScanline = null!;
/// <summary> /// <summary>
/// The current scanline that is being processed. /// The current scanline that is being processed.
/// </summary> /// </summary>
private IMemoryOwner<byte> scanline; private IMemoryOwner<byte> scanline = null!;
/// <summary>
/// The index of the current scanline being processed.
/// </summary>
private int currentRow = Adam7.FirstRow[0];
/// <summary>
/// The current number of bytes read in the current scanline.
/// </summary>
private int currentRowBytesRead;
/// <summary> /// <summary>
/// Gets or sets the png color type. /// Gets or sets the png color type.
@ -148,7 +148,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngMetadata pngMetadata = metadata.GetPngMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream; this.currentStream = stream;
this.currentStream.Skip(8); this.currentStream.Skip(8);
Image<TPixel> image = null; Image<TPixel>? image = null;
APngFrameControl? lastFrameControl = null;
ImageFrame<TPixel>? currentFrame = null;
Span<byte> buffer = stackalloc byte[20]; Span<byte> buffer = stackalloc byte[20];
try try
@ -160,22 +162,84 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
switch (chunk.Type) switch (chunk.Type)
{ {
case PngChunkType.Header: case PngChunkType.Header:
if (!Equals(this.header, default(PngHeader)))
{
PngThrowHelper.ThrowInvalidHeader();
}
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.AnimationControl:
if (this.isSimplePng || this.animationControl is not null)
{
PngThrowHelper.ThrowInvalidAnimationControl();
}
this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Physical: case PngChunkType.Physical:
ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); ReadPhysicalChunk(metadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.Gamma: case PngChunkType.Gamma:
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break; 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) 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; break;
case PngChunkType.Palette: case PngChunkType.Palette:
byte[] pal = new byte[chunk.Length]; byte[] pal = new byte[chunk.Length];
@ -249,6 +313,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ImageMetadata metadata = new(); ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream; this.currentStream = stream;
APngFrameControl? lastFrameControl = null;
Span<byte> buffer = stackalloc byte[20]; Span<byte> buffer = stackalloc byte[20];
this.currentStream.Skip(8); this.currentStream.Skip(8);
@ -264,6 +329,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngChunkType.Header: case PngChunkType.Header:
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.AnimationControl:
if (this.isSimplePng || this.animationControl is not null)
{
PngThrowHelper.ThrowInvalidAnimationControl();
}
this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.Physical: case PngChunkType.Physical:
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
{ {
@ -282,7 +355,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break; 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: case PngChunkType.Data:
if (this.animationControl is null)
{
this.isSimplePng = true;
}
// Spec says tRNS must be before IDAT so safe to exit. // Spec says tRNS must be before IDAT so safe to exit.
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
@ -365,9 +465,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
EOF: 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); return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
@ -398,7 +498,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <param name="bits">The number of bits per value.</param> /// <param name="bits">The number of bits per value.</param>
/// <param name="buffer">The new array.</param> /// <param name="buffer">The new array.</param>
/// <returns>The resulting <see cref="ReadOnlySpan{Byte}"/> array.</returns> /// <returns>The resulting <see cref="ReadOnlySpan{Byte}"/> array.</returns>
private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, out IMemoryOwner<byte> buffer) private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner<byte>? buffer)
{ {
if (bits >= 8) if (bits >= 8)
{ {
@ -433,7 +533,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <param name="data">The data containing physical data.</param> /// <param name="data">The data containing physical data.</param>
private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan<byte> data) private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{ {
PhysicalChunkData physicalChunk = PhysicalChunkData.Parse(data); PngPhysical physicalChunk = PngPhysical.Parse(data);
metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue
? PixelResolutionUnit.AspectRatio ? PixelResolutionUnit.AspectRatio
@ -466,8 +566,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam> /// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <param name="metadata">The metadata information for the image</param> /// <param name="metadata">The metadata information for the image</param>
/// <param name="frameControl">The frame control information for the frame</param>
/// <param name="image">The image that we will populate</param> /// <param name="image">The image that we will populate</param>
private void InitializeImage<TPixel>(ImageMetadata metadata, out Image<TPixel> image) private void InitializeImage<TPixel>(ImageMetadata metadata, APngFrameControl? frameControl, out Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
image = Image.CreateUninitialized<TPixel>( image = Image.CreateUninitialized<TPixel>(
@ -476,6 +577,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header.Height, this.header.Height,
metadata); metadata);
if (frameControl is { } control)
{
APngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAPngFrameMetadata();
frameMetadata.FromChunk(control);
}
this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1; this.bytesPerSample = 1;
@ -490,6 +597,27 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.scanline = this.configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean); this.scanline = this.configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
} }
/// <summary>
/// Initializes the image and various buffers needed for processing
/// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <param name="frameControl">The frame control information for the frame</param>
/// <param name="image">The image that we will populate</param>
private void InitializeFrame<TPixel>(APngFrameControl frameControl, Image<TPixel> image, out ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
frame = image.Frames.CreateFrame();
APngFrameMetadata frameMetadata = frame.Metadata.GetAPngFrameMetadata();
frameMetadata.FromChunk(frameControl);
this.previousScanline?.Dispose();
this.scanline?.Dispose();
this.previousScanline = this.memoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
}
/// <summary> /// <summary>
/// Calculates the correct number of bits per pixel for the given color type. /// Calculates the correct number of bits per pixel for the given color type.
/// </summary> /// </summary>
@ -553,18 +681,19 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Reads the scanlines within the image. /// Reads the scanlines within the image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="chunk">The png chunk containing the compressed scanline data.</param> /// <param name="chunkLength">The length of the chunk that containing the compressed scanline data.</param>
/// <param name="image"> The pixel data.</param> /// <param name="image"> The pixel data.</param>
/// <param name="pngMetadata">The png metadata</param> /// <param name="pngMetadata">The png metadata</param>
/// <param name="getData">A delegate to get more data from the inner stream for <see cref="ZlibInflateStream"/>.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
private void ReadScanlines<TPixel>(PngChunk chunk, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken) private void ReadScanlines<TPixel>(int chunkLength, ImageFrame<TPixel> image, PngMetadata pngMetadata, Func<int> getData, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk); using ZlibInflateStream deframeStream = new(this.currentStream, getData);
deframeStream.AllocateNewBytes(chunk.Length, true); deframeStream.AllocateNewBytes(chunkLength, true);
DeflateStream dataStream = deframeStream.CompressedStream; DeflateStream dataStream = deframeStream.CompressedStream!;
if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{ {
this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken); this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken);
} }
@ -585,22 +714,25 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken) private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
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(); cancellationToken.ThrowIfCancellationRequested();
Span<byte> scanlineSpan = this.scanline.GetSpan(); Span<byte> 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) if (bytesRead <= 0)
{ {
return; return;
} }
this.currentRowBytesRead += bytesRead; currentRowBytesRead += bytesRead;
} }
this.currentRowBytesRead = 0; currentRowBytesRead = 0;
switch ((FilterType)scanlineSpan[0]) switch ((FilterType)scanlineSpan[0])
{ {
@ -628,10 +760,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break; break;
} }
this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata);
this.SwapScanlineBuffers(); this.SwapScanlineBuffers();
this.currentRow++; ++currentRow;
} }
} }
@ -647,8 +779,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken) private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int currentRow = Adam7.FirstRow[0];
int currentRowBytesRead = 0;
int pass = 0; int pass = 0;
int width = this.header.Width; int width = this.header.Width;
int height = this.header.Height;
if (image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata))
{
width = frameMetadata.Width;
height = frameMetadata.Height;
}
Buffer2D<TPixel> imageBuffer = image.PixelBuffer; Buffer2D<TPixel> imageBuffer = image.PixelBuffer;
while (true) while (true)
{ {
@ -656,7 +797,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (numColumns == 0) if (numColumns == 0)
{ {
pass++; ++pass;
// This pass contains no data; skip to next pass // This pass contains no data; skip to next pass
continue; continue;
@ -664,21 +805,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
while (this.currentRow < this.header.Height) while (currentRow < height)
{ {
cancellationToken.ThrowIfCancellationRequested(); 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) if (bytesRead <= 0)
{ {
return; return;
} }
this.currentRowBytesRead += bytesRead; currentRowBytesRead += bytesRead;
} }
this.currentRowBytesRead = 0; currentRowBytesRead = 0;
Span<byte> scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); Span<byte> scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline);
Span<byte> prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); Span<byte> prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline);
@ -709,12 +850,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break; break;
} }
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow); Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
this.SwapScanlineBuffers(); this.SwapScanlineBuffers();
this.currentRow += Adam7.RowIncrement[pass]; currentRow += Adam7.RowIncrement[pass];
} }
pass++; pass++;
@ -722,7 +863,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (pass < 7) if (pass < 7)
{ {
this.currentRow = Adam7.FirstRow[pass]; currentRow = Adam7.FirstRow[pass];
} }
else else
{ {
@ -736,19 +877,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Processes the de-filtered scanline filling the image pixel data /// Processes the de-filtered scanline filling the image pixel data
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="currentRow">The index of the current scanline being processed.</param>
/// <param name="defilteredScanline">The de-filtered scanline</param> /// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="pixels">The image</param> /// <param name="pixels">The image</param>
/// <param name="pngMetadata">The png metadata.</param> /// <param name="pngMetadata">The png metadata.</param>
private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetadata pngMetadata) private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow); Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
// Trim the first marker byte from the buffer // Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline[1..]; ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null; IMemoryOwner<byte>? buffer = null;
try try
{ {
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray( ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(
@ -840,7 +982,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan<byte> trimmed = defilteredScanline[1..]; ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null; IMemoryOwner<byte>? buffer = null;
try try
{ {
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray( ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(
@ -975,6 +1117,31 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
} }
/// <summary>
/// Reads a animation control chunk from the data.
/// </summary>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> data)
{
this.animationControl = APngAnimationControl.Parse(data);
pngMetadata.NumberPlays = this.animationControl.NumberPlays;
}
/// <summary>
/// Reads a header chunk from the data.
/// </summary>
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private APngFrameControl ReadFrameControlChunk(ReadOnlySpan<byte> data)
{
APngFrameControl fcTL = APngFrameControl.Parse(data);
fcTL.Validate(this.header);
return fcTL;
}
/// <summary> /// <summary>
/// Reads a header chunk from the data. /// Reads a header chunk from the data.
/// </summary> /// </summary>
@ -1062,7 +1229,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan<byte> compressedData = data[(zeroIndex + 2)..]; ReadOnlySpan<byte> 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)) && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed))
{ {
metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty));
@ -1355,7 +1522,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
ReadOnlySpan<byte> compressedData = data[dataStartIdx..]; ReadOnlySpan<byte> 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)); pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
} }
@ -1378,7 +1545,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <param name="encoding">The string encoding to use.</param> /// <param name="encoding">The string encoding to use.</param>
/// <param name="value">The uncompressed value.</param> /// <param name="value">The uncompressed value.</param>
/// <returns>The <see cref="bool"/>.</returns> /// <returns>The <see cref="bool"/>.</returns>
private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, out string value) private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, [NotNullWhen(true)] out string? value)
{ {
if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData)) if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
{ {
@ -1407,7 +1574,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (this.TryReadChunk(buffer, out PngChunk chunk)) if (this.TryReadChunk(buffer, out PngChunk chunk))
{ {
if (chunk.Type == PngChunkType.Data) if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData)
{ {
chunk.Data?.Dispose(); chunk.Data?.Dispose();
return chunk.Length; 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. // 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. // 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); chunk = new PngChunk(length, type);
@ -1476,9 +1643,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.ValidateChunk(chunk, buffer); 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. // 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; this.currentStream.Position = pos;
} }

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

@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -647,9 +648,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; 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);
} }
/// <summary> /// <summary>

5
src/ImageSharp/Formats/Png/PngFormat.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the png format. /// Registers the image encoders, decoders and mime type detectors for the png format.
/// </summary> /// </summary>
public sealed class PngFormat : IImageFormat<PngMetadata> public sealed class PngFormat : IImageFormat<PngMetadata, APngFrameMetadata>
{ {
private PngFormat() private PngFormat()
{ {
@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat<PngMetadata>
/// <inheritdoc/> /// <inheritdoc/>
public PngMetadata CreateDefaultFormatMetadata() => new(); public PngMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public APngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
} }

8
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -32,6 +34,7 @@ public class PngMetadata : IDeepCloneable
this.TransparentL16 = other.TransparentL16; this.TransparentL16 = other.TransparentL16;
this.TransparentRgb24 = other.TransparentRgb24; this.TransparentRgb24 = other.TransparentRgb24;
this.TransparentRgb48 = other.TransparentRgb48; this.TransparentRgb48 = other.TransparentRgb48;
this.NumberPlays = other.NumberPlays;
for (int i = 0; i < other.TextData.Count; i++) for (int i = 0; i < other.TextData.Count; i++)
{ {
@ -95,6 +98,11 @@ public class PngMetadata : IDeepCloneable
/// </summary> /// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>(); public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
/// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this); public IDeepCloneable DeepClone() => new PngMetadata(this);
} }

1
src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

@ -4,6 +4,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;

17
src/ImageSharp/Formats/Png/PngThrowHelper.cs

@ -12,13 +12,22 @@ internal static class PngThrowHelper
=> throw new InvalidImageContentException(errorMessage, innerException); => throw new InvalidImageContentException(errorMessage, innerException);
[DoesNotReturn] [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] [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] [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] [DoesNotReturn]
public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); 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!"); public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type"); public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type.");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type.");

1
src/ImageSharp/ImageSharp.csproj

@ -13,6 +13,7 @@
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore</PackageTags> <PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore</PackageTags>
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description> <Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<LangVersion>preview</LangVersion>
</PropertyGroup> </PropertyGroup>
<!-- This enables the nullable analysis and treats all nullable warnings as error--> <!-- This enables the nullable analysis and treats all nullable warnings as error-->

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

@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -132,6 +134,34 @@ public class PngMetadataTests
VerifyExifDataIsPresent(exif); VerifyExifDataIsPresent(exif);
} }
[Theory]
[WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)]
public void Decode_ReadsExifData2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new()
{
SkipMetadata = false
};
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new()
{
SkipMetadata = false
};
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, options);
}
[Theory] [Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue<TPixel>(TestImageProvider<TPixel> provider) public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue<TPixel>(TestImageProvider<TPixel> provider)

3
tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

@ -1,7 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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; namespace SixLabors.ImageSharp.Tests.Formats.Png;

Loading…
Cancel
Save