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 => + ProcessInterlacedRgbaScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedRgbaScanline( - in PngHeader header, + Configuration configuration, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -495,15 +361,15 @@ public static void ProcessInterlacedRgbaScanline( int 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) { 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) { rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -514,11 +380,20 @@ public static void ProcessInterlacedRgbaScanline( Unsafe.Add(ref rowSpanRef, x) = pixel; } } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } else { + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); 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)); diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 67da78e45b..0552e9a79e 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Png; @@ -12,13 +13,22 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except => throw new InvalidImageContentException(errorMessage, innerException); [DoesNotReturn] - public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk"); + public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); [DoesNotReturn] - public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk"); + public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk."); [DoesNotReturn] - public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk"); + public static void ThrowMissingDefaultData() => throw new InvalidImageContentException("APNG Image does not contain a default data chunk."); + + [DoesNotReturn] + public static void ThrowInvalidAnimationControl() => throw new InvalidImageContentException("APNG Image must contain a acTL chunk and it must be located before any IDAT and fdAT chunks."); + + [DoesNotReturn] + public static void ThrowMissingFrameControl() => throw new InvalidImageContentException("One of APNG Image's frames do not have a frame control chunk."); + + [DoesNotReturn] + public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk."); [DoesNotReturn] public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); @@ -30,7 +40,15 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type"); + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") + => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); + + [DoesNotReturn] + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value1))] string name2 = "") + => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); + + [DoesNotReturn] + public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type."); [DoesNotReturn] public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index b32711ebf2..e927fb0fac 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Collections; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Primitives/Rational.cs b/src/ImageSharp/Primitives/Rational.cs index 59f34331a7..201219f7e0 100644 --- a/src/ImageSharp/Primitives/Rational.cs +++ b/src/ImageSharp/Primitives/Rational.cs @@ -70,7 +70,7 @@ public Rational(double value) /// Whether to use the best possible precision when parsing the value. public Rational(double value, bool bestPrecision) { - var rational = LongRational.FromDouble(Math.Abs(value), bestPrecision); + LongRational rational = LongRational.FromDouble(Math.Abs(value), bestPrecision); this.Numerator = (uint)rational.Numerator; this.Denominator = (uint)rational.Denominator; @@ -109,7 +109,7 @@ public Rational(double value, bool bestPrecision) /// /// The . /// - public static Rational FromDouble(double value) => new Rational(value, false); + public static Rational FromDouble(double value) => new(value, false); /// /// Converts the specified to an instance of this type. @@ -119,24 +119,19 @@ public Rational(double value, bool bestPrecision) /// /// The . /// - public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision); + public static Rational FromDouble(double value, bool bestPrecision) => new(value, bestPrecision); /// public override bool Equals(object? obj) => obj is Rational other && this.Equals(other); /// public bool Equals(Rational other) - { - var left = new LongRational(this.Numerator, this.Denominator); - var right = new LongRational(other.Numerator, other.Denominator); - - return left.Equals(right); - } + => this.Numerator == other.Numerator && this.Denominator == other.Denominator; /// public override int GetHashCode() { - var self = new LongRational(this.Numerator, this.Denominator); + LongRational self = new(this.Numerator, this.Denominator); return self.GetHashCode(); } @@ -169,7 +164,7 @@ public override int GetHashCode() /// The public string ToString(IFormatProvider provider) { - var rational = new LongRational(this.Numerator, this.Denominator); + LongRational rational = new(this.Numerator, this.Denominator); return rational.ToString(provider); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index e216832853..2e11093db6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -79,6 +78,18 @@ public partial class PngDecoderTests { TestImages.Png.Rgba64Bpp, typeof(Image) }, }; + public static readonly string[] MultiFrameTestFiles = + { + TestImages.Png.APng, + TestImages.Png.SplitIDatZeroLength, + TestImages.Png.DisposeNone, + TestImages.Png.DisposeBackground, + TestImages.Png.DisposeBackgroundRegion, + TestImages.Png.DisposePreviousFirst, + TestImages.Png.DisposeBackgroundBeforeRegion, + TestImages.Png.BlendOverMultiple + }; + [Theory] [MemberData(nameof(PixelFormatRange))] public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type) @@ -107,6 +118,19 @@ public void Decode(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFileCollection(nameof(MultiFrameTestFiles), PixelTypes.Rgba32)] + public void Decode_VerifyAllFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + + // Some images have many frames, only compare a selection of them. + static bool Predicate(int i, int _) => i <= 8 || i % 8 == 0; + image.DebugSaveMultiFrame(provider, predicate: Predicate); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate); + } + [Theory] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] public void PngDecoder_Decode_Resize(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 3c80cfe098..92c07a27a6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -442,6 +442,40 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color }); } + [Theory] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void Encode_APng(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + using MemoryStream memStream = new(); + image.Save(memStream, PngEncoder); + memStream.Position = 0; + + image.DebugSave(provider: provider, encoder: PngEncoder, null, false); + + using Image output = Image.Load(memStream); + ImageComparer.Exact.VerifySimilarity(output, image); + + Assert.Equal(5, image.Frames.Count); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + PngMetadata originalMetadata = image.Metadata.GetPngMetadata(); + PngMetadata outputMetadata = output.Metadata.GetPngMetadata(); + + Assert.Equal(originalMetadata.RepeatCount, outputMetadata.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngFrameMetadata(); + PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngFrameMetadata(); + + Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay); + Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod); + Assert.Equal(originalFrameMetadata.DisposalMethod, outputFrameMetadata.DisposalMethod); + } + } + [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs new file mode 100644 index 0000000000..e29585c2dc --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Png; + +namespace SixLabors.ImageSharp.Tests.Formats.Png; + +[Trait("Format", "Png")] +public class PngFrameMetadataTests +{ + [Fact] + public void CloneIsDeep() + { + PngFrameMetadata meta = new() + { + FrameDelay = new(1, 0), + DisposalMethod = PngDisposalMethod.Background, + BlendMethod = PngBlendMethod.Over, + }; + + PngFrameMetadata clone = (PngFrameMetadata)meta.DeepClone(); + + Assert.True(meta.FrameDelay.Equals(clone.FrameDelay)); + Assert.True(meta.DisposalMethod.Equals(clone.DisposalMethod)); + Assert.True(meta.BlendMethod.Equals(clone.BlendMethod)); + + clone.FrameDelay = new(2, 1); + clone.DisposalMethod = PngDisposalMethod.Previous; + clone.BlendMethod = PngBlendMethod.Source; + + Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); + Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); + Assert.False(meta.BlendMethod.Equals(clone.BlendMethod)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index d7a353665a..b3c122a7a8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -30,15 +31,25 @@ public void CloneIsDeep() ColorType = PngColorType.GrayscaleWithAlpha, InterlaceMethod = PngInterlaceMode.Adam7, Gamma = 2, - TextData = new List { new PngTextData("name", "value", "foo", "bar") } + TextData = new List { new PngTextData("name", "value", "foo", "bar") }, + RepeatCount = 123 }; PngMetadata clone = (PngMetadata)meta.DeepClone(); + Assert.True(meta.BitDepth == clone.BitDepth); + Assert.True(meta.ColorType == clone.ColorType); + Assert.True(meta.InterlaceMethod == clone.InterlaceMethod); + Assert.True(meta.Gamma.Equals(clone.Gamma)); + Assert.False(meta.TextData.Equals(clone.TextData)); + Assert.True(meta.TextData.SequenceEqual(clone.TextData)); + Assert.True(meta.RepeatCount == clone.RepeatCount); + clone.BitDepth = PngBitDepth.Bit2; clone.ColorType = PngColorType.Palette; clone.InterlaceMethod = PngInterlaceMode.None; clone.Gamma = 1; + clone.RepeatCount = 321; Assert.False(meta.BitDepth == clone.BitDepth); Assert.False(meta.ColorType == clone.ColorType); @@ -46,6 +57,7 @@ public void CloneIsDeep() Assert.False(meta.Gamma.Equals(clone.Gamma)); Assert.False(meta.TextData.Equals(clone.TextData)); Assert.True(meta.TextData.SequenceEqual(clone.TextData)); + Assert.False(meta.RepeatCount == clone.RepeatCount); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 04341a2419..878f3fb8d4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; namespace SixLabors.ImageSharp.Tests.Formats.Png; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c5565bbd85..048b19dc5b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -62,6 +62,17 @@ public static class Png public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string XmpColorPalette = "Png/xmp-colorpalette.png"; + // Animated + // https://philip.html5.org/tests/apng/tests.html + public const string APng = "Png/animated/apng.png"; + public const string SplitIDatZeroLength = "Png/animated/4-split-idat-zero-length.png"; + public const string DisposeNone = "Png/animated/7-dispose-none.png"; + public const string DisposeBackground = "Png/animated/8-dispose-background.png"; + public const string DisposeBackgroundBeforeRegion = "Png/animated/14-dispose-background-before-region.png"; + public const string DisposeBackgroundRegion = "Png/animated/15-dispose-background-region.png"; + public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png"; + public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png"; + // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; public const string SubFilter3BytesPerPixel = "Png/filter1.png"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 3601344ee3..9b100047f0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -179,7 +179,7 @@ public string SaveTestOutputFile( return path; } - public IEnumerable GetTestOutputFileNamesMultiFrame( + public IEnumerable<(int Index, string FileName)> GetTestOutputFileNamesMultiFrame( int frameCount, string extension = null, object testOutputDetails = null, @@ -201,11 +201,11 @@ public IEnumerable GetTestOutputFileNamesMultiFrame( continue; } - yield return $"{baseDir}/{i:D2}.{extension}"; + yield return (i, $"{baseDir}/{i:D2}.{extension}"); } } - public string[] SaveTestOutputFileMultiFrame( + public (int Index, string FileName)[] SaveTestOutputFileMultiFrame( Image image, string extension = "png", IImageEncoder encoder = null, @@ -216,27 +216,17 @@ public string[] SaveTestOutputFileMultiFrame( { encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}"); - string[] files = this.GetTestOutputFileNamesMultiFrame( + (int Index, string FileName)[] files = this.GetTestOutputFileNamesMultiFrame( image.Frames.Count, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate).ToArray(); - for (int i = 0; i < image.Frames.Count; i++) + foreach ((int Index, string FileName) file in files) { - if (predicate != null && !predicate(i, image.Frames.Count)) - { - continue; - } - - if (i >= files.Length) - { - break; - } - - using Image frameImage = image.Frames.CloneFrame(i); - string filePath = files[i]; + using Image frameImage = image.Frames.CloneFrame(file.Index); + string filePath = file.FileName; using FileStream stream = File.OpenWrite(filePath); frameImage.Save(stream, encoder); } @@ -252,14 +242,14 @@ internal string GetReferenceOutputFileName( => TestEnvironment.GetReferenceOutputFileName( this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription)); - public string[] GetReferenceOutputFileNamesMultiFrame( + public (int Index, string FileName)[] GetReferenceOutputFileNamesMultiFrame( int frameCount, string extension, object testOutputDetails, bool appendPixelTypeToFileName = true, Func predicate = null) => this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate) - .Select(TestEnvironment.GetReferenceOutputFileName).ToArray(); + .Select(x => (x.Index, TestEnvironment.GetReferenceOutputFileName(x.FileName))).ToArray(); internal void Init(string typeName, string methodName, string outputSubfolderName) { diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 9951ecfa90..3c74b48938 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -336,7 +336,7 @@ public static Image GetReferenceOutputImageMultiFrame( Func predicate = null) where TPixel : unmanaged, IPixel { - string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( + (int Index, string FileName)[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( frameCount, extension, testOutputDetails, @@ -345,10 +345,11 @@ public static Image GetReferenceOutputImageMultiFrame( List> temporaryFrameImages = new(); - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0].FileName); - foreach (string path in frameFiles) + for (int i = 0; i < frameFiles.Length; i++) { + string path = frameFiles[i].FileName; if (!File.Exists(path)) { throw new FileNotFoundException("Reference output file missing: " + path); @@ -536,10 +537,8 @@ public static Image CompareToOriginal( referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream)) - { - comparer.VerifySimilarity(original, image); - } + using Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream); + comparer.VerifySimilarity(original, image); return image; } @@ -562,10 +561,8 @@ public static Image CompareToOriginalMultiFrame( referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(DecoderOptions.Default, stream)) - { - comparer.VerifySimilarity(original, image); - } + using Image original = referenceDecoder.Decode(DecoderOptions.Default, stream); + comparer.VerifySimilarity(original, image); return image; } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 974e951f6f..3dceaf2524 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -200,13 +200,13 @@ public void SaveTestOutputFileMultiFrame(TestImageProvider provi where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); + (int Index, string FileName)[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); Assert.True(files.Length > 2); - foreach (string path in files) + foreach ((int Index, string FileName) file in files) { - this.Output.WriteLine(path); - Assert.True(File.Exists(path)); + this.Output.WriteLine(file.FileName); + Assert.True(File.Exists(file.FileName)); } } diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png new file mode 100644 index 0000000000..7f10ed9664 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38 +size 99 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png new file mode 100644 index 0000000000..7f10ed9664 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38 +size 99 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png new file mode 100644 index 0000000000..de47c015c9 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1322aa335ad845cacfa20266bc0ffc31db117376373c15bcdb222abcf4b8f83 +size 113 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png new file mode 100644 index 0000000000..7b8766bdc5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6118abf41302696bfe4a62baa32a7798b3833ca49fc3854dcde4a810905fc457 +size 1012 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png new file mode 100644 index 0000000000..097c9b76f9 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8d11d84cab8580efc7397870116ff3ddde4c3a5da9c2c2baa473eb463326072 +size 915 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png new file mode 100644 index 0000000000..47148a78e6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f36ea3ed9e652fe005c2767d758da268feb444e90833e02ab3fb15d1155037fd +size 971 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png new file mode 100644 index 0000000000..ff550fcfbb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb61715535a98977f4a3cb89ac85bc56826a54b4bdd4393d89ca445f50865d22 +size 990 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png new file mode 100644 index 0000000000..12233f3015 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0648a06346a6ccca69503da187bc5901c7275ade03834030a8f3895ad03ff58a +size 941 diff --git a/tests/Images/Input/Png/animated/12-dispose-prev-first.png b/tests/Images/Input/Png/animated/12-dispose-prev-first.png new file mode 100644 index 0000000000..7d6c9db25d --- /dev/null +++ b/tests/Images/Input/Png/animated/12-dispose-prev-first.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28138dd4a4ad56f86c18216b051b96a1bb353b69ebd85ce272928b085bb84400 +size 371 diff --git a/tests/Images/Input/Png/animated/14-dispose-background-before-region.png b/tests/Images/Input/Png/animated/14-dispose-background-before-region.png new file mode 100644 index 0000000000..3411044e6d --- /dev/null +++ b/tests/Images/Input/Png/animated/14-dispose-background-before-region.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3d4ba499c333a600dd1e42f374a9a68fb783b0f3274091ab34f5b395462eae8 +size 327 diff --git a/tests/Images/Input/Png/animated/15-dispose-background-region.png b/tests/Images/Input/Png/animated/15-dispose-background-region.png new file mode 100644 index 0000000000..8e684686c9 --- /dev/null +++ b/tests/Images/Input/Png/animated/15-dispose-background-region.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6db2a90911b40067b7f35b01869115f081858ee15b28374e57c51c7e5c0cb524 +size 492 diff --git a/tests/Images/Input/Png/animated/21-blend-over-multiple.png b/tests/Images/Input/Png/animated/21-blend-over-multiple.png new file mode 100644 index 0000000000..4c088bacc4 --- /dev/null +++ b/tests/Images/Input/Png/animated/21-blend-over-multiple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b571f7034ef1fb355182cf00fa6ccd7d784720709f229e3bcc5948abf2f81ee +size 28791 diff --git a/tests/Images/Input/Png/animated/4-split-idat-zero-length.png b/tests/Images/Input/Png/animated/4-split-idat-zero-length.png new file mode 100644 index 0000000000..d2d6567462 --- /dev/null +++ b/tests/Images/Input/Png/animated/4-split-idat-zero-length.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ffdbe7dc6dad05dfc4cacd712b76c1121cd7378671212ae000d76c07b1a4e +size 273 diff --git a/tests/Images/Input/Png/animated/7-dispose-none.png b/tests/Images/Input/Png/animated/7-dispose-none.png new file mode 100644 index 0000000000..d0ef09b852 --- /dev/null +++ b/tests/Images/Input/Png/animated/7-dispose-none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abab0c7de5252a16da34777ff34c4a29c6000493d23ac1777cd17415e6aab33 +size 617 diff --git a/tests/Images/Input/Png/animated/8-dispose-background.png b/tests/Images/Input/Png/animated/8-dispose-background.png new file mode 100644 index 0000000000..89052b655d --- /dev/null +++ b/tests/Images/Input/Png/animated/8-dispose-background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f26f544d5f7f0c8d4448ca020c93f79b64e1d607c7c561082bc989ca2e91fad +size 572 diff --git a/tests/Images/Input/Png/animated/apng.png b/tests/Images/Input/Png/animated/apng.png new file mode 100644 index 0000000000..7def301ae6 --- /dev/null +++ b/tests/Images/Input/Png/animated/apng.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c15e4670da1826d1cc25555bd6cbe287ecc70327cd029a7613334a39a283021 +size 2508