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/>
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;
}

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

2
src/ImageSharp/Formats/Png/PngTextData.cs → 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;
/// <summary>
/// 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.
// 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
/// <summary>
/// Gets the png format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngMetadata"/>.</returns>
public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance);
public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="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
/// </summary>
public bool IsCritical =>
this.Type == PngChunkType.Header ||
this.Type == PngChunkType.Palette ||
this.Type == PngChunkType.Data;
this.Type is PngChunkType.Header or
PngChunkType.Palette or
PngChunkType.Data or
PngChunkType.FrameData;
}

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

@ -8,16 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary>
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>
/// 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.
/// </summary>
/// <remarks>IDAT</remarks>
Data = 0x49444154U,
/// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
/// </summary>
/// <remarks>IEND</remarks>
End = 0x49454E44U,
/// <summary>
@ -25,34 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
/// </summary>
/// <remarks>IHDR</remarks>
Header = 0x49484452U,
/// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
/// <remarks>PLTE</remarks>
Palette = 0x504C5445U,
/// <summary>
/// The eXIf data chunk which contains the Exif profile.
/// </summary>
/// <remarks>eXIF</remarks>
Exif = 0x65584966U,
/// <summary>
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
/// </summary>
/// <remarks>gAMA</remarks>
Gamma = 0x67414D41U,
/// <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>
/// <remarks>pHYs</remarks>
Physical = 0x70485973U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>tEXT</remarks>
Text = 0x74455874U,
/// <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
/// a compressed text string.
/// </summary>
/// <remarks>zTXt</remarks>
CompressedText = 0x7A545874U,
/// <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.
/// </summary>
/// <remarks>iTXt</remarks>
InternationalText = 0x69545874U,
/// <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)
/// or a single transparent color (for grayscale and true color images).
/// </summary>
/// <remarks>tRNS</remarks>
Transparency = 0x74524E53U,
/// <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>
/// <remarks>tIME</remarks>
Time = 0x74494d45,
/// <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),
/// the bKGD chunk should be ignored.
/// </summary>
/// <remarks>bKGD</remarks>
Background = 0x624b4744,
/// <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.
/// </summary>
/// <remarks>iCCP</remarks>
EmbeddedColorProfile = 0x69434350,
/// <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.
/// </summary>
/// <remarks>sBIT</remarks>
SignificantBits = 0x73424954,
/// <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.
/// </summary>
/// <remarks>sRGB</remarks>
StandardRgbColourSpace = 0x73524742,
/// <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>
/// <remarks>hIST</remarks>
Histogram = 0x68495354,
/// <summary>
/// The sPLT chunk contains the suggested palette.
/// This chunk contains the suggested palette.
/// </summary>
/// <remarks>sPLT</remarks>
SuggestedPalette = 0x73504c54,
/// <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.
/// </summary>
/// <remarks>cHRM</remarks>
Chroma = 0x6348524d,
/// <summary>
/// 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
/// </summary>
/// <remarks>CgBI</remarks>
ProprietaryApple = 0x43674249
}

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

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

283
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;
/// </summary>
internal sealed class PngDecoderCore : IImageDecoderInternals
{
/// <summary>
/// Indicate whether the file is a simple PNG.
/// </summary>
private bool isSimplePng;
/// <summary>
/// The general decoder options.
/// </summary>
@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream currentStream;
private BufferedReadStream currentStream = null!;
/// <summary>
/// The png header.
/// </summary>
private PngHeader header;
/// <summary>
/// The png animation control.
/// </summary>
private APngAnimationControl? animationControl;
/// <summary>
/// The number of bytes per pixel.
/// </summary>
@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary>
/// The palette containing color information for indexed png's.
/// </summary>
private byte[] palette;
private byte[] palette = null!;
/// <summary>
/// The palette containing alpha channel color information for indexed png's.
/// </summary>
private byte[] paletteAlpha;
private byte[] paletteAlpha = null!;
/// <summary>
/// Previous scanline processed.
/// </summary>
private IMemoryOwner<byte> previousScanline;
private IMemoryOwner<byte> previousScanline = null!;
/// <summary>
/// The current scanline that is being processed.
/// </summary>
private IMemoryOwner<byte> scanline;
/// <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;
private IMemoryOwner<byte> scanline = null!;
/// <summary>
/// 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<TPixel> image = null;
Image<TPixel>? image = null;
APngFrameControl? lastFrameControl = null;
ImageFrame<TPixel>? currentFrame = null;
Span<byte> 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<byte> 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
/// <param name="bits">The number of bits per value.</param>
/// <param name="buffer">The new array.</param>
/// <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)
{
@ -433,7 +533,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <param name="data">The data containing physical data.</param>
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
? PixelResolutionUnit.AspectRatio
@ -466,8 +566,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <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>
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>
{
image = Image.CreateUninitialized<TPixel>(
@ -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<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>
/// Calculates the correct number of bits per pixel for the given color type.
/// </summary>
@ -553,18 +681,19 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Reads the scanlines within the image.
/// </summary>
/// <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="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>
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>
{
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<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
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();
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)
{
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<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> 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<byte> scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline);
Span<byte> prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline);
@ -709,12 +850,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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.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
/// </summary>
/// <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="pixels">The image</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>
{
Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow);
Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
// Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null;
IMemoryOwner<byte>? buffer = null;
try
{
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(
@ -840,7 +982,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null;
IMemoryOwner<byte>? buffer = null;
try
{
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>
/// Reads a header chunk from the data.
/// </summary>
@ -1062,7 +1229,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
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))
{
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..];
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
/// <param name="encoding">The string encoding to use.</param>
/// <param name="value">The uncompressed value.</param>
/// <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))
{
@ -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;
}

5
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);
}
/// <summary>

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

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

8
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
/// </summary>
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/>
public IDeepCloneable DeepClone() => new PngMetadata(this);
}

1
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;

17
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.");

1
src/ImageSharp/ImageSharp.csproj

@ -13,6 +13,7 @@
<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>
<Configurations>Debug;Release</Configurations>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<!-- 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.
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<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]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
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.
// 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;

Loading…
Cancel
Save