diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
index f05f237576..1d743bf3a5 100644
--- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
+++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
@@ -123,12 +123,12 @@ public override int ReadByte()
///
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 @@ public override int Read(byte[] buffer, int offset, int count)
// Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0;
offset += totalBytesRead;
- while (this.currentDataRemaining == 0 && totalBytesRead < count)
+ while (this.currentDataRemaining is 0 && totalBytesRead < count)
{
this.currentDataRemaining = this.getData();
- if (this.currentDataRemaining == 0)
+ if (this.currentDataRemaining is 0)
{
return totalBytesRead;
}
diff --git a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
new file mode 100644
index 0000000000..a9f99a9e4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
+
+internal readonly struct AnimationControl
+{
+ public const int Size = 8;
+
+ public AnimationControl(int numberFrames, int numberPlays)
+ {
+ this.NumberFrames = numberFrames;
+ this.NumberPlays = numberPlays;
+ }
+
+ ///
+ /// Gets the number of frames
+ ///
+ public int NumberFrames { get; }
+
+ ///
+ /// Gets the number of times to loop this APNG. 0 indicates infinite looping.
+ ///
+ public int NumberPlays { get; }
+
+ ///
+ /// 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 AnimationControl Parse(ReadOnlySpan data)
+ => new(
+ numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
+ numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
+}
diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
new file mode 100644
index 0000000000..fb2ca473c2
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
@@ -0,0 +1,160 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
+
+internal readonly struct FrameControl
+{
+ public const int Size = 26;
+
+ public FrameControl(uint width, uint height)
+ : this(0, width, height, 0, 0, 0, 0, default, default)
+ {
+ }
+
+ public FrameControl(
+ uint sequenceNumber,
+ uint width,
+ uint height,
+ uint xOffset,
+ uint yOffset,
+ ushort delayNumerator,
+ ushort delayDenominator,
+ PngDisposalMethod disposeOperation,
+ PngBlendMethod blendOperation)
+ {
+ this.SequenceNumber = sequenceNumber;
+ this.Width = width;
+ this.Height = height;
+ this.XOffset = xOffset;
+ this.YOffset = yOffset;
+ this.DelayNumerator = delayNumerator;
+ this.DelayDenominator = delayDenominator;
+ this.DisposeOperation = disposeOperation;
+ this.BlendOperation = blendOperation;
+ }
+
+ ///
+ /// Gets the sequence number of the animation chunk, starting from 0
+ ///
+ public uint SequenceNumber { get; }
+
+ ///
+ /// Gets the width of the following frame
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets the height of the following frame
+ ///
+ public uint Height { get; }
+
+ ///
+ /// Gets the X position at which to render the following frame
+ ///
+ public uint XOffset { get; }
+
+ ///
+ /// Gets the Y position at which to render the following frame
+ ///
+ public uint YOffset { get; }
+
+ ///
+ /// Gets the X limit at which to render the following frame
+ ///
+ public uint XMax => this.XOffset + this.Width;
+
+ ///
+ /// Gets the Y limit at which to render the following frame
+ ///
+ public uint YMax => this.YOffset + this.Height;
+
+ ///
+ /// Gets the frame delay fraction numerator
+ ///
+ public ushort DelayNumerator { get; }
+
+ ///
+ /// Gets the frame delay fraction denominator
+ ///
+ public ushort DelayDenominator { get; }
+
+ ///
+ /// Gets the type of frame area disposal to be done after rendering this frame
+ ///
+ public PngDisposalMethod DisposeOperation { get; }
+
+ ///
+ /// Gets the type of frame area rendering for this frame
+ ///
+ public PngBlendMethod BlendOperation { get; }
+
+ public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height);
+
+ ///
+ /// Validates the APng fcTL.
+ ///
+ /// The header.
+ ///
+ /// Thrown if the image does pass validation.
+ ///
+ public void Validate(PngHeader header)
+ {
+ if (this.Width == 0)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0");
+ }
+
+ if (this.Height == 0)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0");
+ }
+
+ if (this.XMax > header.Width)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
+ }
+
+ if (this.YMax > header.Height)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}");
+ }
+ }
+
+ ///
+ /// Writes the fcTL to the given buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.SequenceNumber);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[4..8], this.Width);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[8..12], this.Height);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], this.XOffset);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], this.YOffset);
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator);
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[22..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 FrameControl Parse(ReadOnlySpan data)
+ => new(
+ sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
+ width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]),
+ height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]),
+ xOffset: BinaryPrimitives.ReadUInt32BigEndian(data[12..16]),
+ yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]),
+ delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]),
+ delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
+ disposeOperation: (PngDisposalMethod)data[24],
+ blendOperation: (PngBlendMethod)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 @@ public PhysicalChunkData(uint x, uint y, byte unitSpecifier)
///
/// 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 @@ public static PhysicalChunkData Parse(ReadOnlySpan data)
///
/// 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 @@ public static PhysicalChunkData FromMetadata(ImageMetadata meta)
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..f24b8d1b5c 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,22 @@ 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 PngFrameMetadata GetPngFrameMetadata(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 TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
}
diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs
new file mode 100644
index 0000000000..f71dce8325
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs
@@ -0,0 +1,22 @@
+// 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 PngBlendMethod
+{
+ ///
+ /// 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].
+ ///
+ Over
+}
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 @@ public PngChunk(int length, PngChunkType type, IMemoryOwner data = null)
/// 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..a008bf8ea2 100644
--- a/src/ImageSharp/Formats/Png/PngChunkType.cs
+++ b/src/ImageSharp/Formats/Png/PngChunkType.cs
@@ -9,15 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal enum PngChunkType : uint
{
///
- /// The IDAT chunk contains the actual image data. The image can contains more
+ /// This 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 (Multiple)
Data = 0x49444154U,
///
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
///
+ /// IEND (Single)
End = 0x49454E44U,
///
@@ -25,34 +27,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
///
+ /// IHDR (Single)
Header = 0x49484452U,
///
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
///
+ /// PLTE (Single)
Palette = 0x504C5445U,
///
/// The eXIf data chunk which contains the Exif profile.
///
+ /// eXIF (Single)
Exif = 0x65584966U,
///
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
///
+ /// gAMA (Single)
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 (Single)
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 (Multiple)
Text = 0x74455874U,
///
@@ -60,70 +68,103 @@ 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 (Multiple)
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 (Multiple)
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 (Single)
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 (Single)
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 (Single)
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 (Single)
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 (Single)
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 (Single)
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 (Single)
Histogram = 0x68495354,
///
- /// The sPLT chunk contains the suggested palette.
+ /// This chunk contains the suggested palette.
///
+ /// sPLT (Single)
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 (Single)
Chroma = 0x6348524d,
+ ///
+ /// This chunk is an ancillary chunk as defined in the PNG Specification.
+ /// It must appear before the first IDAT chunk within a valid PNG stream.
+ ///
+ /// acTL (Single, APNG)
+ AnimationControl = 0x6163544cU,
+
+ ///
+ /// This chunk is an ancillary chunk as defined in the PNG Specification.
+ /// It must appear before the IDAT or fdAT chunks of the frame to which it applies.
+ ///
+ /// fcTL (Multiple, APNG)
+ FrameControl = 0x6663544cU,
+
+ ///
+ /// This chunk has the same purpose as an IDAT chunk.
+ /// It has the same structure as an IDAT chunk, except preceded by a sequence number.
+ ///
+ /// fdAT (Multiple, APNG)
+ FrameData = 0x66644154U,
+
///
/// 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..43f2b0fb25 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,7 +80,7 @@ internal static class PngConstants
///
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
///
- public static ReadOnlySpan XmpKeyword => new byte[]
+ public static ReadOnlySpan XmpKeyword => new[]
{
(byte)'X',
(byte)'M',
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 065d861e71..d8305a3f57 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;
@@ -34,12 +34,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private readonly Configuration configuration;
///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
+ /// Whether the metadata should be ignored when the image is being decoded.
+ ///
+ private readonly uint maxFrames;
+
+ ///
+ /// Whether the metadata should be ignored when the image is being decoded.
///
private readonly bool skipMetadata;
///
- /// Gets or sets a value indicating whether to read the IHDR and tRNS chunks only.
+ /// Whether to read the IHDR and tRNS chunks only.
///
private readonly bool colorMetadataOnly;
@@ -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 AnimationControl 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.
@@ -121,6 +121,7 @@ public PngDecoderCore(DecoderOptions options)
{
this.Options = options;
this.configuration = options.Configuration;
+ this.maxFrames = options.MaxFrames;
this.skipMetadata = options.SkipMetadata;
this.memoryAllocator = this.configuration.MemoryAllocator;
}
@@ -129,6 +130,7 @@ internal PngDecoderCore(DecoderOptions options, bool colorMetadataOnly)
{
this.Options = options;
this.colorMetadataOnly = colorMetadataOnly;
+ this.maxFrames = options.MaxFrames;
this.skipMetadata = true;
this.configuration = options.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
@@ -144,11 +146,16 @@ internal PngDecoderCore(DecoderOptions options, bool colorMetadataOnly)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ uint frameCount = 0;
ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
this.currentStream.Skip(8);
- Image image = null;
+ Image? image = null;
+ FrameControl? previousFrameControl = null;
+ FrameControl? currentFrameControl = null;
+ ImageFrame? previousFrame = null;
+ ImageFrame? currentFrame = null;
Span buffer = stackalloc byte[20];
try
@@ -160,25 +167,84 @@ public Image Decode(BufferedReadStream stream, CancellationToken
switch (chunk.Type)
{
case PngChunkType.Header:
+ if (!Equals(this.header, default(PngHeader)))
+ {
+ PngThrowHelper.ThrowInvalidHeader();
+ }
+
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.AnimationControl:
+ 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.FrameControl:
+ frameCount++;
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ currentFrame = null;
+ currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ if (image is null)
+ {
+ PngThrowHelper.ThrowMissingDefaultData();
+ }
+
+ if (currentFrameControl is null)
+ {
+ PngThrowHelper.ThrowMissingFrameControl();
+ }
+
+ previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
+ this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame);
+
+ this.currentStream.Position += 4;
+ this.ReadScanlines(
+ chunk.Length - 4,
+ currentFrame,
+ pngMetadata,
+ this.ReadNextDataChunkAndSkipSeq,
+ currentFrameControl.Value,
+ cancellationToken);
+
+ previousFrame = currentFrame;
+ previousFrameControl = currentFrameControl;
+ break;
case PngChunkType.Data:
+
+ currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
{
- this.InitializeImage(metadata, out image);
+ this.InitializeImage(metadata, currentFrameControl.Value, out image);
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
}
- this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken);
+ this.ReadScanlines(
+ chunk.Length,
+ image.Frames.RootFrame,
+ pngMetadata,
+ this.ReadNextDataChunk,
+ currentFrameControl.Value,
+ cancellationToken);
+ previousFrame = currentFrame;
+ previousFrameControl = currentFrameControl;
break;
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
@@ -245,9 +311,11 @@ public Image Decode(BufferedReadStream stream, CancellationToken
///
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
+ uint frameCount = 0;
ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
+ FrameControl? lastFrameControl = null;
Span buffer = stackalloc byte[20];
this.currentStream.Skip(8);
@@ -263,6 +331,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
case PngChunkType.Header:
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.AnimationControl:
+ this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
+ break;
case PngChunkType.Physical:
if (this.colorMetadataOnly)
{
@@ -281,8 +352,36 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
- case PngChunkType.Data:
+ case PngChunkType.FrameControl:
+ ++frameCount;
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+ 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:
// Spec says tRNS must be before IDAT so safe to exit.
if (this.colorMetadataOnly)
{
@@ -369,7 +468,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
EOF:
if (this.header.Width == 0 && this.header.Height == 0)
{
- PngThrowHelper.ThrowNoHeader();
+ PngThrowHelper.ThrowInvalidHeader();
}
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
@@ -403,7 +502,7 @@ private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset)
/// 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)
{
@@ -438,7 +537,7 @@ private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanli
/// 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
@@ -471,8 +570,9 @@ private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan d
///
/// 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, FrameControl frameControl, out Image image)
where TPixel : unmanaged, IPixel
{
image = Image.CreateUninitialized(
@@ -481,6 +581,9 @@ private void InitializeImage(ImageMetadata metadata, out Image i
this.header.Height,
metadata);
+ PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata();
+ frameMetadata.FromChunk(in frameControl);
+
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
@@ -495,6 +598,47 @@ private void InitializeImage(ImageMetadata metadata, out Image i
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 previous frame.
+ /// The frame control information for the current frame.
+ /// The image that we will populate
+ /// The previous frame.
+ /// The created frame
+ private void InitializeFrame(
+ FrameControl previousFrameControl,
+ FrameControl currentFrameControl,
+ Image image,
+ ImageFrame? previousFrame,
+ out ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ // We create a clone of the previous frame and add it.
+ // We will overpaint the difference of pixels on the current frame to create a complete image.
+ // This ensures that we have enough pixel data to process without distortion. #2450
+ frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame);
+
+ // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
+ if (previousFrameControl.DisposeOperation == PngDisposalMethod.Background
+ || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.Previous))
+ {
+ Rectangle restoreArea = previousFrameControl.Bounds;
+ Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
+ Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
+ pixelRegion.Clear();
+ }
+
+ PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata();
+ frameMetadata.FromChunk(currentFrameControl);
+
+ 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.
///
@@ -558,24 +702,32 @@ private int CalculateScanlineLength(int width)
/// 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 frame control
/// 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,
+ in FrameControl frameControl,
+ 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 inflateStream = new(this.currentStream, getData);
+ inflateStream.AllocateNewBytes(chunkLength, true);
+ DeflateStream dataStream = inflateStream.CompressedStream!;
- if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
+ if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{
- this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken);
+ this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
else
{
- this.DecodePixelData(dataStream, image, pngMetadata, cancellationToken);
+ this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
}
@@ -583,29 +735,48 @@ private void ReadScanlines(PngChunk chunk, ImageFrame image, Png
/// Decodes the raw pixel data row by row
///
/// The pixel format.
+ /// The frame control
/// The compressed pixel data stream.
- /// The image to decode to.
+ /// The image frame to decode to.
/// The png metadata
/// The CancellationToken
- private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void DecodePixelData(
+ FrameControl frameControl,
+ DeflateStream compressedStream,
+ ImageFrame imageFrame,
+ PngMetadata pngMetadata,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- while (this.currentRow < this.header.Height)
+ int currentRow = (int)frameControl.YOffset;
+ int currentRowBytesRead = 0;
+ int height = (int)frameControl.YMax;
+
+ IMemoryOwner? blendMemory = null;
+ Span blendRowBuffer = Span.Empty;
+ if (frameControl.BlendOperation == PngBlendMethod.Over)
+ {
+ blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean);
+ blendRowBuffer = blendMemory.Memory.Span;
+ }
+
+ while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
- Span scanlineSpan = this.scanline.GetSpan();
- while (this.currentRowBytesRead < this.bytesPerScanline)
+ int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1;
+ Span scanlineSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
+ while (currentRowBytesRead < bytesPerFrameScanline)
{
- int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
+ int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
}
- this.currentRowBytesRead += bytesRead;
+ currentRowBytesRead += bytesRead;
}
- this.currentRowBytesRead = 0;
+ currentRowBytesRead = 0;
switch ((FilterType)scanlineSpan[0])
{
@@ -633,28 +804,47 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame<
break;
}
- this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata);
-
+ this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata, blendRowBuffer);
this.SwapScanlineBuffers();
- this.currentRow++;
+ currentRow++;
}
+
+ blendMemory?.Dispose();
}
///
/// Decodes the raw interlaced pixel data row by row
- ///
///
/// The pixel format.
+ /// The frame control
/// The compressed pixel data stream.
- /// The current image.
+ /// The current image frame.
/// The png metadata.
/// The cancellation token.
- private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void DecodeInterlacedPixelData(
+ in FrameControl frameControl,
+ DeflateStream compressedStream,
+ ImageFrame imageFrame,
+ PngMetadata pngMetadata,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset;
+ int currentRowBytesRead = 0;
int pass = 0;
- int width = this.header.Width;
- Buffer2D imageBuffer = image.PixelBuffer;
+ int width = (int)frameControl.Width;
+ int endRow = (int)frameControl.YMax;
+
+ Buffer2D imageBuffer = imageFrame.PixelBuffer;
+
+ IMemoryOwner? blendMemory = null;
+ Span blendRowBuffer = Span.Empty;
+ if (frameControl.BlendOperation == PngBlendMethod.Over)
+ {
+ blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean);
+ blendRowBuffer = blendMemory.Memory.Span;
+ }
+
while (true)
{
int numColumns = Adam7.ComputeColumns(width, pass);
@@ -669,21 +859,21 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
- while (this.currentRow < this.header.Height)
+ while (currentRow < endRow)
{
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);
@@ -714,12 +904,20 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I
break;
}
- Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow);
- this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
-
+ Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
+ this.ProcessInterlacedDefilteredScanline(
+ frameControl,
+ this.scanline.GetSpan(),
+ rowSpan,
+ pngMetadata,
+ blendRowBuffer,
+ pixelOffset: Adam7.FirstColumn[pass],
+ increment: Adam7.ColumnIncrement[pass]);
+
+ blendRowBuffer.Clear();
this.SwapScanlineBuffers();
- this.currentRow += Adam7.RowIncrement[pass];
+ currentRow += Adam7.RowIncrement[pass];
}
pass++;
@@ -727,7 +925,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I
if (pass < 7)
{
- this.currentRow = Adam7.FirstRow[pass];
+ currentRow = Adam7.FirstRow[pass];
}
else
{
@@ -735,27 +933,44 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I
break;
}
}
+
+ blendMemory?.Dispose();
}
///
/// Processes the de-filtered scanline filling the image pixel data
///
/// The pixel format.
- /// The de-filtered scanline
+ /// The frame control
+ /// 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)
+ /// A span used to temporarily hold the decoded row pixel data for alpha blending.
+ private void ProcessDefilteredScanline(
+ in FrameControl frameControl,
+ int currentRow,
+ ReadOnlySpan scanline,
+ ImageFrame pixels,
+ PngMetadata pngMetadata,
+ Span blendRowBuffer)
where TPixel : unmanaged, IPixel
{
- Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow);
+ Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
+
+ bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
+ Span rowSpan = blend
+ ? blendRowBuffer
+ : destination;
// Trim the first marker byte from the buffer
- ReadOnlySpan trimmed = defilteredScanline[1..];
+ ReadOnlySpan trimmed = scanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- IMemoryOwner buffer = null;
+ IMemoryOwner? buffer = null;
try
{
+ // TODO: The allocation here could be per frame, not per scanline.
ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
trimmed,
this.bytesPerScanline - 1,
@@ -768,7 +983,8 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessGrayscaleScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
pngMetadata.TransparentColor);
@@ -777,7 +993,8 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)this.bytesPerPixel,
@@ -787,7 +1004,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
case PngColorType.Palette:
PngScanlineProcessor.ProcessPaletteScanline(
- this.header,
+ in frameControl,
scanlineSpan,
rowSpan,
pngMetadata.ColorTable);
@@ -797,7 +1014,8 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
case PngColorType.Rgb:
PngScanlineProcessor.ProcessRgbScanline(
this.configuration,
- this.header,
+ this.header.BitDepth,
+ frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
@@ -809,7 +1027,8 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessRgbaScanline(
this.configuration,
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
@@ -817,6 +1036,13 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
break;
}
+
+ if (blend)
+ {
+ PixelBlender blender =
+ PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
+ blender.Blend(this.configuration, destination, destination, rowSpan, 1f);
+ }
}
finally
{
@@ -828,19 +1054,33 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
/// Processes the interlaced de-filtered scanline filling the image pixel data
///
/// The pixel format.
- /// The de-filtered scanline
- /// The current image row.
+ /// The frame control
+ /// The de-filtered scanline
+ /// The current image row.
/// The png metadata.
+ /// A span used to temporarily hold the decoded row pixel data for alpha blending.
/// The column start index. Always 0 for none interlaced images.
/// The column increment. Always 1 for none interlaced images.
- private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1)
+ private void ProcessInterlacedDefilteredScanline(
+ in FrameControl frameControl,
+ ReadOnlySpan scanline,
+ Span destination,
+ PngMetadata pngMetadata,
+ Span blendRowBuffer,
+ int pixelOffset = 0,
+ int increment = 1)
where TPixel : unmanaged, IPixel
{
+ bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
+ Span rowSpan = blend
+ ? blendRowBuffer
+ : destination;
+
// Trim the first marker byte from the buffer
- ReadOnlySpan trimmed = defilteredScanline[1..];
+ ReadOnlySpan trimmed = scanline[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(
@@ -855,7 +1095,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -866,7 +1107,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -878,7 +1120,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
case PngColorType.Palette:
PngScanlineProcessor.ProcessInterlacedPaletteScanline(
- this.header,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -889,7 +1131,9 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
case PngColorType.Rgb:
PngScanlineProcessor.ProcessInterlacedRgbScanline(
- this.header,
+ this.configuration,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -902,7 +1146,9 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessInterlacedRgbaScanline(
- this.header,
+ this.configuration,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -912,6 +1158,13 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
break;
}
+
+ if (blend)
+ {
+ PixelBlender blender =
+ PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
+ blender.Blend(this.configuration, destination, destination, rowSpan, 1f);
+ }
}
finally
{
@@ -996,6 +1249,31 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM
}
}
+ ///
+ /// Reads a animation control chunk from the data.
+ ///
+ /// The png metadata.
+ /// The containing data.
+ private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data)
+ {
+ this.animationControl = AnimationControl.Parse(data);
+
+ pngMetadata.RepeatCount = this.animationControl.NumberPlays;
+ }
+
+ ///
+ /// Reads a header chunk from the data.
+ ///
+ /// The containing data.
+ private FrameControl ReadFrameControlChunk(ReadOnlySpan data)
+ {
+ FrameControl fcTL = FrameControl.Parse(data);
+
+ fcTL.Validate(this.header);
+
+ return fcTL;
+ }
+
///
/// Reads a header chunk from the data.
///
@@ -1083,7 +1361,7 @@ private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata met
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));
@@ -1376,7 +1654,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, 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));
}
@@ -1399,7 +1677,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpanThe 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))
{
@@ -1424,11 +1702,11 @@ private int ReadNextDataChunk()
Span buffer = stackalloc byte[20];
- this.currentStream.Read(buffer, 0, 4);
+ _ = this.currentStream.Read(buffer, 0, 4);
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;
@@ -1440,6 +1718,22 @@ private int ReadNextDataChunk()
return 0;
}
+ ///
+ /// Reads the next data chunk and skip sequence number.
+ ///
+ /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left.
+ private int ReadNextDataChunkAndSkipSeq()
+ {
+ int length = this.ReadNextDataChunk();
+ if (this.ReadNextDataChunk() is 0)
+ {
+ return length;
+ }
+
+ this.currentStream.Position += 4; // Skip sequence number
+ return length - 4;
+ }
+
///
/// Reads a chunk from the stream.
///
@@ -1497,9 +1791,9 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk)
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/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
new file mode 100644
index 0000000000..17391de95c
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.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 PngDisposalMethod
+{
+ ///
+ /// 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/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index c16348b8d9..04e3b1d840 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using System.Buffers;
using System.Buffers.Binary;
@@ -100,18 +99,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
/// The raw data of previous scanline.
///
- private IMemoryOwner previousScanline;
+ private IMemoryOwner previousScanline = null!;
///
/// The raw data of current scanline.
///
- private IMemoryOwner currentScanline;
+ private IMemoryOwner currentScanline = null!;
///
/// The color profile name.
///
private const string ColorProfileName = "ICC Profile";
+ ///
+ /// The encoder quantizer, if present.
+ ///
+ private IQuantizer? quantizer;
+
///
/// Initializes a new instance of the class.
///
@@ -122,6 +126,7 @@ public PngEncoderCore(Configuration configuration, PngEncoder encoder)
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
+ this.quantizer = encoder.Quantizer;
}
///
@@ -141,20 +146,23 @@ public void Encode(Image image, Stream stream, CancellationToken
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
-
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
- Image clonedImage = null;
- bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
+
+ stream.Write(PngConstants.HeaderBytes);
+
+ ImageFrame? clonedFrame = null;
+ ImageFrame currentFrame = image.Frames.RootFrame;
+
+ bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
{
- clonedImage = image.Clone();
- ClearTransparentPixels(clonedImage);
+ currentFrame = clonedFrame = currentFrame.Clone();
+ ClearTransparentPixels(currentFrame);
}
- IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
-
- stream.Write(PngConstants.HeaderBytes);
+ // Do not move this. We require an accurate bit depth for the header chunk.
+ IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
@@ -165,13 +173,58 @@ public void Encode(Image image, Stream stream, CancellationToken
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
- this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
+
+ if (image.Frames.Count > 1)
+ {
+ this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.RepeatCount);
+
+ // TODO: We should attempt to optimize the output by clipping the indexed result to
+ // non-transparent bounds. That way we can assign frame control bounds and encode
+ // less data. See GifEncoder for the implementation there.
+
+ // Write the first frame.
+ FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0);
+ this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+
+ // Capture the global palette for reuse on subsequent frames.
+ ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
+
+ // Write following frames.
+ uint increment = 0;
+ for (int i = 1; i < image.Frames.Count; i++)
+ {
+ currentFrame = image.Frames[i];
+ if (clearTransparency)
+ {
+ // Dispose of previous clone and reassign.
+ clonedFrame?.Dispose();
+ currentFrame = clonedFrame = currentFrame.Clone();
+ ClearTransparentPixels(currentFrame);
+ }
+
+ // Each frame control sequence number must be incremented by the
+ // number of frame data chunks that follow.
+ frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment);
+
+ // Dispose of previous quantized frame and reassign.
+ quantized?.Dispose();
+ quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette);
+ increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true);
+ }
+ }
+ else
+ {
+ FrameControl frameControl = new((uint)this.width, (uint)this.height);
+ this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+ }
+
this.WriteEndChunk(stream);
stream.Flush();
+ // Dispose of allocations from final frame.
+ clonedFrame?.Dispose();
quantized?.Dispose();
- clonedImage?.Dispose();
}
///
@@ -179,18 +232,16 @@ public void Dispose()
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
- this.previousScanline = null;
- this.currentScanline = null;
}
///
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
///
/// The type of the pixel.
- /// The cloned image where the transparent pixels will be changed.
- private static void ClearTransparentPixels(Image image)
- where TPixel : unmanaged, IPixel =>
- image.ProcessPixelRows(accessor =>
+ /// The cloned image frame where the transparent pixels will be changed.
+ private static void ClearTransparentPixels(ImageFrame clone)
+ where TPixel : unmanaged, IPixel
+ => clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 rgba32 = default;
@@ -202,7 +253,7 @@ private static void ClearTransparentPixels(Image image)
{
span[x].ToRgba32(ref rgba32);
- if (rgba32.A == 0)
+ if (rgba32.A is 0)
{
span[x].FromRgba32(transparent);
}
@@ -214,24 +265,17 @@ private static void ClearTransparentPixels(Image image)
/// Creates the quantized image and calculates and sets the bit depth.
///
/// The type of the pixel.
- /// The image to quantize.
- /// Cloned image with transparent pixels are changed to black.
+ /// The image metadata.
+ /// The frame to quantize.
+ /// Any previously derived palette.
/// The quantized image.
- private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth(
- Image image,
- Image clonedImage)
+ private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth(
+ PngMetadata metadata,
+ ImageFrame frame,
+ ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
- IndexedImageFrame quantized;
- if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
- }
- else
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
- }
-
+ IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
@@ -242,9 +286,7 @@ private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth(
private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
where TPixel : unmanaged, IPixel
{
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span rawScanlineSpan = this.currentScanline.GetSpan();
- ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.colorType == PngColorType.Grayscale)
{
@@ -400,20 +442,19 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan)
/// The row span.
/// The quantized pixels. Can be null.
/// The row.
- private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row)
+ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame? quantized, int row)
where TPixel : unmanaged, IPixel
{
switch (this.colorType)
{
case PngColorType.Palette:
-
if (this.bitDepth < 8)
{
- PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(quantized!.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
- quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
+ quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
@@ -474,7 +515,7 @@ private void CollectAndFilterPixelRow(
ReadOnlySpan rowSpan,
ref Span filter,
ref Span attempt,
- IndexedImageFrame quantized,
+ IndexedImageFrame? quantized,
int row)
where TPixel : unmanaged, IPixel
{
@@ -574,6 +615,21 @@ private void WriteHeaderChunk(Stream stream)
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
}
+ ///
+ /// Writes the animation control chunk to the stream.
+ ///
+ /// The containing image data.
+ /// The number of frames.
+ /// The number of times to loop this APNG.
+ private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount)
+ {
+ AnimationControl acTL = new(framesCount, playsCount);
+
+ acTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size);
+ }
+
///
/// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk.
@@ -581,7 +637,7 @@ private void WriteHeaderChunk(Stream stream)
/// The pixel format.
/// The containing image data.
/// The quantized frame.
- private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized)
+ private void WritePaletteChunk(Stream stream, IndexedImageFrame? quantized)
where TPixel : unmanaged, IPixel
{
if (quantized is null)
@@ -640,14 +696,14 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame
/// The image metadata.
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{
- if ((this.chunkFilter & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
+ if (this.chunkFilter.HasFlag(PngChunkFilter.ExcludePhysicalChunk))
{
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);
}
///
@@ -689,9 +745,9 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta)
return;
}
- byte[] xmpData = meta.XmpProfile.Data;
+ byte[]? xmpData = meta.XmpProfile.Data;
- if (xmpData.Length == 0)
+ if (xmpData?.Length is 0 or null)
{
return;
}
@@ -758,18 +814,9 @@ private void WriteTextChunks(Stream stream, PngMetadata meta)
}
const int maxLatinCode = 255;
- for (int i = 0; i < meta.TextData.Count; i++)
+ foreach (PngTextData textData in meta.TextData)
{
- PngTextData textData = meta.TextData[i];
- bool hasUnicodeCharacters = false;
- foreach (char c in textData.Value)
- {
- if (c > maxLatinCode)
- {
- hasUnicodeCharacters = true;
- break;
- }
- }
+ bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode);
if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))
{
@@ -932,14 +979,45 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
}
}
+ ///
+ /// Writes the animation control chunk to the stream.
+ ///
+ /// The containing image data.
+ /// The image frame.
+ /// The frame sequence number.
+ private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
+ {
+ PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
+
+ // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
+ FrameControl fcTL = new(
+ sequenceNumber: sequenceNumber,
+ width: (uint)imageFrame.Width,
+ height: (uint)imageFrame.Height,
+ xOffset: 0,
+ yOffset: 0,
+ delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
+ delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
+ disposeOperation: frameMetadata.DisposalMethod,
+ blendOperation: frameMetadata.BlendMethod);
+
+ fcTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size);
+
+ return fcTL;
+ }
+
///
/// Writes the pixel information to the stream.
///
/// The pixel format.
- /// The image.
+ /// The frame control
+ /// The frame.
/// The quantized pixel data. Can be null.
/// The stream.
- private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream)
+ /// Is writing fdAT or IDAT.
+ private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -949,20 +1027,20 @@ private void WriteDataChunks(Image pixels, IndexedImageFrame(Image pixels, IndexedImageFrame MaxBlockSize)
+ if (length > maxBlockSize)
{
- length = MaxBlockSize;
+ length = maxBlockSize;
}
- this.WriteChunk(stream, PngChunkType.Data, buffer, i * MaxBlockSize, length);
+ if (isFrame)
+ {
+ // We increment the sequence number for each frame chunk.
+ // '1' is added to the sequence number to account for the preceding frame control chunk.
+ uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i);
+ this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length);
+ }
+ else
+ {
+ this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length);
+ }
}
+
+ return (uint)numChunks;
}
///
@@ -1009,13 +1105,17 @@ private void AllocateScanlineBuffers(int bytesPerScanline)
/// Encodes the pixels.
///
/// The type of the pixel.
+ /// The frame control
/// The pixels.
/// The quantized pixels span.
/// The deflate stream.
- private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int bytesPerScanline = this.CalculateScanlineLength(this.width);
+ int width = (int)frameControl.Width;
+ int height = (int)frameControl.Height;
+
+ int bytesPerScanline = this.CalculateScanlineLength(width);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
@@ -1026,7 +1126,7 @@ private void EncodePixels(Image pixels, IndexedImageFrame filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
- for (int y = 0; y < this.height; y++)
+ for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++)
{
this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter);
@@ -1039,18 +1139,19 @@ private void EncodePixels(Image pixels, IndexedImageFrame
/// The type of the pixel.
- /// The image.
+ /// The frame control
+ /// The image frame.
/// The deflate stream.
- private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
- Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer;
+ int width = (int)frameControl.XMax;
+ int height = (int)frameControl.YMax;
+ Buffer2D pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass];
- int startCol = Adam7.FirstColumn[pass];
+ int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
+ int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
@@ -1072,7 +1173,7 @@ private void EncodeAdam7Pixels(Image image, ZlibDeflateStream de
{
// Collect pixel data
Span srcRow = pixelBuffer.DangerousGetRowSpan(row);
- for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass])
+ for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass])
{
block[i++] = srcRow[col];
}
@@ -1092,17 +1193,18 @@ private void EncodeAdam7Pixels(Image image, ZlibDeflateStream de
/// Interlaced encoding the quantized (indexed, with palette) pixels.
///
/// The type of the pixel.
+ /// The frame control
/// The quantized.
/// The deflate stream.
- private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7IndexedPixels(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = quantized.Width;
- int height = quantized.Height;
+ int width = (int)frameControl.Width;
+ int endRow = (int)frameControl.YMax;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass];
- int startCol = Adam7.FirstColumn[pass];
+ int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
+ int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
@@ -1121,17 +1223,16 @@ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantize
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
- for (int row = startRow;
- row < height;
- row += Adam7.RowIncrement[pass])
+ for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass])
{
// Collect data
ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row);
for (int col = startCol, i = 0;
- col < width;
+ col < frameControl.XMax;
col += Adam7.ColumnIncrement[pass])
{
- block[i++] = srcRow[col];
+ block[i] = srcRow[col];
+ i++;
}
// Encode data
@@ -1163,7 +1264,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data)
///
/// The to write to.
/// The type of chunk to write.
- /// The containing data.
+ /// The containing data.
/// The position to offset the data at.
/// The of the data to write.
private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length)
@@ -1189,6 +1290,38 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o
stream.Write(buffer, 0, 4); // write the crc
}
+ ///
+ /// Writes a frame data chunk of a specified length to the stream at the given offset.
+ ///
+ /// The to write to.
+ /// The frame sequence number.
+ /// The containing data.
+ /// The position to offset the data at.
+ /// The of the data to write.
+ private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span data, int offset, int length)
+ {
+ Span buffer = stackalloc byte[12];
+
+ BinaryPrimitives.WriteInt32BigEndian(buffer, length + 4);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)PngChunkType.FrameData);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8, 4), sequenceNumber);
+
+ stream.Write(buffer);
+
+ uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer
+
+ if (data.Length > 0 && length > 0)
+ {
+ stream.Write(data, offset, length);
+
+ crc = Crc32.Calculate(crc, data.Slice(offset, length));
+ }
+
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, crc);
+
+ stream.Write(buffer, 0, 4); // write the crc
+ }
+
///
/// Calculates the scanline length.
///
@@ -1198,7 +1331,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o
///
private int CalculateScanlineLength(int width)
{
- int mod = this.bitDepth == 16 ? 16 : 8;
+ int mod = this.bitDepth is 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod;
@@ -1242,14 +1375,7 @@ private void SanitizeAndSetEncoderOptions(
if (!encoder.FilterMethod.HasValue)
{
// Specification recommends default filter method None for paletted images and Paeth for others.
- if (this.colorType == PngColorType.Palette)
- {
- this.filterMethod = PngFilterMethod.None;
- }
- else
- {
- this.filterMethod = PngFilterMethod.Paeth;
- }
+ this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
}
// Ensure bit depth and color type are a supported combination.
@@ -1265,7 +1391,7 @@ private void SanitizeAndSetEncoderOptions(
use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
- this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod).Value;
+ this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
}
@@ -1276,40 +1402,50 @@ private void SanitizeAndSetEncoderOptions(
/// The png encoder.
/// The color type.
/// The bits per component.
- /// The image.
- private static IndexedImageFrame CreateQuantizedFrame(
+ /// The image metadata.
+ /// The frame to quantize.
+ /// Any previously derived palette.
+ private IndexedImageFrame? CreateQuantizedFrame(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
- Image image)
+ PngMetadata metadata,
+ ImageFrame frame,
+ ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
- if (colorType != PngColorType.Palette)
+ if (colorType is not PngColorType.Palette)
{
return null;
}
+ if (previousPalette is not null)
+ {
+ // Use the previously derived palette created by quantizing the root frame to quantize the current frame.
+ using PaletteQuantizer paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1);
+ paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
+ return paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
+ }
+
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
- IQuantizer quantizer = encoder.Quantizer;
- if (quantizer is null)
+ if (this.quantizer is null)
{
- PngMetadata metadata = image.Metadata.GetPngMetadata();
if (metadata.ColorTable is not null)
{
- // Use the provided palette in total. The caller is responsible for setting values.
- quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
+ // Use the provided palette. The caller is responsible for setting values.
+ this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
}
else
{
- quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
+ this.quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
}
}
// Create quantized frame returning the palette and set the bit depth.
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.Configuration);
+ using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration);
- frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image);
- return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
+ return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
///
@@ -1323,25 +1459,23 @@ private static IndexedImageFrame CreateQuantizedFrame(
private static byte CalculateBitDepth(
PngColorType colorType,
byte bitDepth,
- IndexedImageFrame quantizedFrame)
+ IndexedImageFrame? quantizedFrame)
where TPixel : unmanaged, IPixel
{
- if (colorType == PngColorType.Palette)
+ if (colorType is PngColorType.Palette)
{
- byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length), 1, 8);
+ byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame!.Palette.Length), 1, 8);
byte bits = Math.Max(bitDepth, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range.
- if (bits == 3)
+ bits = bits switch
{
- bits = 4;
- }
- else if (bits is >= 5 and <= 7)
- {
- bits = 8;
- }
+ 3 => 4,
+ >= 5 and <= 7 => 8,
+ _ => bits
+ };
bitDepth = bits;
}
@@ -1379,21 +1513,21 @@ private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16
/// The type of pixel format.
private static PngColorType SuggestColorType()
where TPixel : unmanaged, IPixel
- => typeof(TPixel) switch
+ => default(TPixel) switch
{
- Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(Bgr24) => PngColorType.Rgb,
- Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(L8) => PngColorType.Grayscale,
- Type t when t == typeof(L16) => PngColorType.Grayscale,
- Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(Rgb24) => PngColorType.Rgb,
- Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(Rgb48) => PngColorType.Rgb,
- Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha,
+ A8 => PngColorType.GrayscaleWithAlpha,
+ Argb32 => PngColorType.RgbWithAlpha,
+ Bgr24 => PngColorType.Rgb,
+ Bgra32 => PngColorType.RgbWithAlpha,
+ L8 => PngColorType.Grayscale,
+ L16 => PngColorType.Grayscale,
+ La16 => PngColorType.GrayscaleWithAlpha,
+ La32 => PngColorType.GrayscaleWithAlpha,
+ Rgb24 => PngColorType.Rgb,
+ Rgba32 => PngColorType.RgbWithAlpha,
+ Rgb48 => PngColorType.Rgb,
+ Rgba64 => PngColorType.RgbWithAlpha,
+ RgbaVector => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha
};
@@ -1404,27 +1538,27 @@ private static PngColorType SuggestColorType()
/// The type of pixel format.
private static PngBitDepth SuggestBitDepth()
where TPixel : unmanaged, IPixel
- => typeof(TPixel) switch
+ => default(TPixel) switch
{
- Type t when t == typeof(A8) => PngBitDepth.Bit8,
- Type t when t == typeof(Argb32) => PngBitDepth.Bit8,
- Type t when t == typeof(Bgr24) => PngBitDepth.Bit8,
- Type t when t == typeof(Bgra32) => PngBitDepth.Bit8,
- Type t when t == typeof(L8) => PngBitDepth.Bit8,
- Type t when t == typeof(L16) => PngBitDepth.Bit16,
- Type t when t == typeof(La16) => PngBitDepth.Bit8,
- Type t when t == typeof(La32) => PngBitDepth.Bit16,
- Type t when t == typeof(Rgb24) => PngBitDepth.Bit8,
- Type t when t == typeof(Rgba32) => PngBitDepth.Bit8,
- Type t when t == typeof(Rgb48) => PngBitDepth.Bit16,
- Type t when t == typeof(Rgba64) => PngBitDepth.Bit16,
- Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16,
+ A8 => PngBitDepth.Bit8,
+ Argb32 => PngBitDepth.Bit8,
+ Bgr24 => PngBitDepth.Bit8,
+ Bgra32 => PngBitDepth.Bit8,
+ L8 => PngBitDepth.Bit8,
+ L16 => PngBitDepth.Bit16,
+ La16 => PngBitDepth.Bit8,
+ La32 => PngBitDepth.Bit16,
+ Rgb24 => PngBitDepth.Bit8,
+ Rgba32 => PngBitDepth.Bit8,
+ Rgb48 => PngBitDepth.Bit16,
+ Rgba64 => PngBitDepth.Bit16,
+ RgbaVector => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8
};
private unsafe struct ScratchBuffer
{
- private const int Size = 16;
+ private const int Size = 26;
private fixed byte scratch[Size];
public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs
index 2d1f2dcc7d..e5852affa9 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 @@ private PngFormat()
///
public PngMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
new file mode 100644
index 0000000000..ca4d8c1f45
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -0,0 +1,62 @@
+// 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 PngFrameMetadata : IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PngFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ private PngFrameMetadata(PngFrameMetadata other)
+ {
+ this.FrameDelay = other.FrameDelay;
+ this.DisposalMethod = other.DisposalMethod;
+ this.BlendMethod = other.BlendMethod;
+ }
+
+ ///
+ /// Gets or sets the frame delay for animated images.
+ /// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to
+ /// wait before continuing with the processing of the Data Stream.
+ /// The clock starts ticking immediately after the graphic is rendered.
+ ///
+ public Rational FrameDelay { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area disposal to be done after rendering this frame
+ ///
+ public PngDisposalMethod DisposalMethod { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area rendering for this frame
+ ///
+ public PngBlendMethod BlendMethod { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chunk to create an instance from.
+ internal void FromChunk(in FrameControl frameControl)
+ {
+ this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
+ this.DisposalMethod = frameControl.DisposeOperation;
+ this.BlendMethod = frameControl.BlendOperation;
+ }
+
+ ///
+ public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
+}
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 8806c29b1a..b113dbfc17 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Png.Chunks;
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats.Png;
///
@@ -26,6 +29,7 @@ private PngMetadata(PngMetadata other)
this.Gamma = other.Gamma;
this.InterlaceMethod = other.InterlaceMethod;
this.TransparentColor = other.TransparentColor;
+ this.RepeatCount = other.RepeatCount;
if (other.ColorTable?.Length > 0)
{
@@ -75,6 +79,11 @@ private PngMetadata(PngMetadata other)
///
public IList TextData { get; set; } = new List();
+ ///
+ /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
+ ///
+ public int RepeatCount { 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 b0afd9975e..f217515e3c 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;
@@ -15,75 +16,24 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal static class PngScanlineProcessor
{
public static void ProcessGrayscaleScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
Color? transparentColor)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
-
- if (transparentColor is null)
- {
- if (header.BitDepth == 16)
- {
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
- {
- ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- pixel.FromL16(Unsafe.As(ref luminance));
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
- pixel.FromL8(Unsafe.As(ref luminance));
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
-
- return;
- }
-
- if (header.BitDepth == 16)
- {
- L16 transparent = transparentColor.Value.ToPixel();
- La32 source = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
- {
- ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- source.L = luminance;
- source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue;
-
- pixel.FromLa32(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
- La16 source = default;
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
- source.L = luminance;
- source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
-
- pixel.FromLa16(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedGrayscaleScanline(
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ transparentColor);
public static void ProcessInterlacedGrayscaleScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -91,17 +41,18 @@ public static void ProcessInterlacedGrayscaleScanline(
Color? transparentColor)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
+ int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1);
if (transparentColor is null)
{
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
pixel.FromL16(Unsafe.As(ref luminance));
@@ -110,7 +61,7 @@ public static void ProcessInterlacedGrayscaleScanline(
}
else
{
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
+ for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
pixel.FromL8(Unsafe.As(ref luminance));
@@ -121,12 +72,12 @@ public static void ProcessInterlacedGrayscaleScanline(
return;
}
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
L16 transparent = transparentColor.Value.ToPixel();
La32 source = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance;
@@ -140,7 +91,7 @@ public static void ProcessInterlacedGrayscaleScanline(
{
byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
La16 source = default;
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
+ for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
source.L = luminance;
@@ -153,47 +104,26 @@ public static void ProcessInterlacedGrayscaleScanline(
}
public static void ProcessGrayscaleWithAlphaScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint bytesPerPixel,
uint bytesPerSample)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (header.BitDepth == 16)
- {
- La32 source = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 4)
- {
- source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
-
- pixel.FromLa32(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- La16 source = default;
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- nuint offset = x * bytesPerPixel;
- source.L = Unsafe.Add(ref scanlineSpanRef, offset);
- source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
-
- pixel.FromLa16(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedGrayscaleWithAlphaScanline(
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ bytesPerPixel,
+ bytesPerSample);
public static void ProcessInterlacedGrayscaleWithAlphaScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -202,15 +132,16 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline(
uint bytesPerSample)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
La32 source = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 4)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 4)
{
source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
@@ -222,46 +153,35 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline(
else
{
La16 source = default;
- nuint offset = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment)
+ nuint offset2 = 0;
+ for (nuint x = offset; x < frameControl.XMax; x += increment)
{
- source.L = Unsafe.Add(ref scanlineSpanRef, offset);
- source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
+ source.L = Unsafe.Add(ref scanlineSpanRef, offset2);
+ source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample);
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
- offset += bytesPerPixel;
+ offset2 += bytesPerPixel;
}
}
}
public static void ProcessPaletteScanline(
- in PngHeader header,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
ReadOnlyMemory? palette)
- where TPixel : unmanaged, IPixel
- {
- if (palette is null)
- {
- PngThrowHelper.ThrowMissingPalette();
- }
-
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
-
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- uint index = Unsafe.Add(ref scanlineSpanRef, x);
- pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedPaletteScanline(
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ palette);
public static void ProcessInterlacedPaletteScanline(
- in PngHeader header,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -279,7 +199,7 @@ public static void ProcessInterlacedPaletteScanline(
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
+ for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++)
{
uint index = Unsafe.Add(ref scanlineSpanRef, o);
pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
@@ -289,82 +209,30 @@ public static void ProcessInterlacedPaletteScanline(
public static void ProcessRgbScanline(
Configuration configuration,
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
int bytesPerPixel,
int bytesPerSample,
Color? transparentColor)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (transparentColor is null)
- {
- if (header.BitDepth == 16)
- {
- Rgb48 rgb48 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
-
- pixel.FromRgb48(rgb48);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width);
- }
-
- return;
- }
-
- if (header.BitDepth == 16)
- {
- Rgb48 transparent = transparentColor.Value.ToPixel();
-
- Rgb48 rgb48 = default;
- Rgba64 rgba64 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
-
- rgba64.Rgb = rgb48;
- rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
-
- pixel.FromRgba64(rgba64);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- Rgb24 transparent = transparentColor.Value.ToPixel();
-
- Rgba32 rgba32 = default;
- ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan);
- ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
- rgba32.Rgb = rgb24;
- rgba32.A = rgb24.Equals(transparent) ? byte.MinValue : byte.MaxValue;
-
- pixel.FromRgba32(rgba32);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedRgbScanline(
+ configuration,
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ bytesPerPixel,
+ bytesPerSample,
+ transparentColor);
public static void ProcessInterlacedRgbScanline(
- in PngHeader header,
+ Configuration configuration,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -374,18 +242,19 @@ public static void ProcessInterlacedRgbScanline(
Color? transparentColor)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
+
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- bool hasTransparency = transparentColor is not null;
if (transparentColor is null)
{
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
Rgb48 rgb48 = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@@ -395,11 +264,19 @@ public static void ProcessInterlacedRgbScanline(
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
+ else if (pixelOffset == 0 && increment == 1)
+ {
+ PixelOperations.Instance.FromRgb24Bytes(
+ configuration,
+ scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)],
+ rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width),
+ (int)frameControl.Width);
+ }
else
{
Rgb24 rgb = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
@@ -413,14 +290,14 @@ public static void ProcessInterlacedRgbScanline(
return;
}
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
Rgb48 transparent = transparentColor.Value.ToPixel();
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@@ -439,7 +316,7 @@ public static void ProcessInterlacedRgbScanline(
Rgba32 rgba = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
@@ -454,39 +331,28 @@ public static void ProcessInterlacedRgbScanline(
public static void ProcessRgbaScanline(
Configuration configuration,
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
int bytesPerPixel,
int bytesPerSample)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (header.BitDepth == 16)
- {
- Rgba64 rgba64 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
- rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
-
- pixel.FromRgba64(rgba64);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width);
- }
- }
+ where TPixel : unmanaged, IPixel