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