From 45f6f5b1548ac08e7ef2d5e6dab71e06f3a272e0 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 12 Aug 2023 08:31:22 +0800 Subject: [PATCH 01/20] Implement APNG decoder --- .../Compression/Zlib/ZlibInflateStream.cs | 8 +- .../Formats/Png/APngBlendOperation.cs | 20 ++ .../Formats/Png/APngDisposeOperation.cs | 25 ++ .../Formats/Png/APngFrameMetadata.cs | 94 ++++++ .../Png/Chunks/APngAnimationControl.cs | 43 +++ .../Formats/Png/Chunks/APngFrameControl.cs | 152 ++++++++++ .../Formats/Png/{ => Chunks}/PngHeader.cs | 2 +- .../{PhysicalChunkData.cs => PngPhysical.cs} | 14 +- .../Formats/Png/{ => Chunks}/PngTextData.cs | 2 +- .../Formats/Png/MetadataExtensions.cs | 21 +- src/ImageSharp/Formats/Png/PngChunk.cs | 7 +- src/ImageSharp/Formats/Png/PngChunkType.cs | 57 +++- src/ImageSharp/Formats/Png/PngConstants.cs | 27 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 283 ++++++++++++++---- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +- src/ImageSharp/Formats/Png/PngFormat.cs | 5 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 8 + .../Formats/Png/PngScanlineProcessor.cs | 1 + src/ImageSharp/Formats/Png/PngThrowHelper.cs | 17 +- src/ImageSharp/ImageSharp.csproj | 1 + .../Formats/Png/PngMetadataTests.cs | 30 ++ .../Formats/Png/PngTextDataTests.cs | 3 +- 22 files changed, 707 insertions(+), 118 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/APngBlendOperation.cs create mode 100644 src/ImageSharp/Formats/Png/APngDisposeOperation.cs create mode 100644 src/ImageSharp/Formats/Png/APngFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs create mode 100644 src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs rename src/ImageSharp/Formats/Png/{ => Chunks}/PngHeader.cs (98%) rename src/ImageSharp/Formats/Png/Chunks/{PhysicalChunkData.cs => PngPhysical.cs} (89%) rename src/ImageSharp/Formats/Png/{ => Chunks}/PngTextData.cs (99%) diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index 06a7c3928c..c9f9904363 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -123,12 +123,12 @@ 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/APngBlendOperation.cs b/src/ImageSharp/Formats/Png/APngBlendOperation.cs new file mode 100644 index 0000000000..0e8cdb4289 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngBlendOperation.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer. +/// +public enum APngBlendOperation +{ + /// + /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. + /// + Source, + + /// + /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. Note that the second variation of the sample code is applicable. + /// + Over +} diff --git a/src/ImageSharp/Formats/Png/APngDisposeOperation.cs b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs new file mode 100644 index 0000000000..7b39a220d3 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). +/// +public enum APngDisposeOperation +{ + /// + /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. + /// + None, + + /// + /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + /// + Background, + + /// + /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. + /// + Previous +} diff --git a/src/ImageSharp/Formats/Png/APngFrameMetadata.cs b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs new file mode 100644 index 0000000000..f4f5fec916 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Png.Chunks; + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Provides APng specific metadata information for the image frame. +/// +public class APngFrameMetadata : IDeepCloneable +{ + /// + /// Initializes a new instance of the class. + /// + public APngFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private APngFrameMetadata(APngFrameMetadata other) + { + this.Width = other.Width; + this.Height = other.Height; + this.XOffset = other.XOffset; + this.YOffset = other.YOffset; + this.DelayNumber = other.DelayNumber; + this.DelayDenominator = other.DelayDenominator; + this.DisposeOperation = other.DisposeOperation; + this.BlendOperation = other.BlendOperation; + } + + /// + /// Gets or sets the width of the following frame + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of the following frame + /// + public int Height { get; set; } + + /// + /// Gets or sets the X position at which to render the following frame + /// + public int XOffset { get; set; } + + /// + /// Gets or sets the Y position at which to render the following frame + /// + public int YOffset { get; set; } + + /// + /// Gets or sets the frame delay fraction numerator + /// + public short DelayNumber { get; set; } + + /// + /// Gets or sets the frame delay fraction denominator + /// + public short DelayDenominator { get; set; } + + /// + /// Gets or sets the type of frame area disposal to be done after rendering this frame + /// + public APngDisposeOperation DisposeOperation { get; set; } + + /// + /// Gets or sets the type of frame area rendering for this frame + /// + public APngBlendOperation BlendOperation { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The chunk to create an instance from. + internal void FromChunk(APngFrameControl frameControl) + { + this.Width = frameControl.Width; + this.Height = frameControl.Height; + this.XOffset = frameControl.XOffset; + this.YOffset = frameControl.YOffset; + this.DelayNumber = frameControl.DelayNumber; + this.DelayDenominator = frameControl.DelayDenominator; + this.DisposeOperation = frameControl.DisposeOperation; + this.BlendOperation = frameControl.BlendOperation; + } + + /// + public IDeepCloneable DeepClone() => new APngFrameMetadata(this); +} diff --git a/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs new file mode 100644 index 0000000000..ca8268cd5d --- /dev/null +++ b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks; + +internal record APngAnimationControl( + int NumberFrames, + int NumberPlays) +{ + public const int Size = 8; + + /// + /// Gets the number of frames + /// + public int NumberFrames { get; } = NumberFrames; + + /// + /// Gets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public int NumberPlays { get; } = NumberPlays; + + /// + /// Writes the acTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays); + } + + /// + /// Parses the APngAnimationControl from the given data buffer. + /// + /// The data to parse. + /// The parsed acTL. + public static APngAnimationControl Parse(ReadOnlySpan data) + => new( + NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8])); +} diff --git a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs new file mode 100644 index 0000000000..e239bd8e2e --- /dev/null +++ b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks; + +internal readonly struct APngFrameControl +{ + public const int Size = 26; + + public APngFrameControl( + int sequenceNumber, + int width, + int height, + int xOffset, + int yOffset, + short delayNumber, + short delayDenominator, + APngDisposeOperation disposeOperation, + APngBlendOperation blendOperation) + { + this.SequenceNumber = sequenceNumber; + this.Width = width; + this.Height = height; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.DelayNumber = delayNumber; + this.DelayDenominator = delayDenominator; + this.DisposeOperation = disposeOperation; + this.BlendOperation = blendOperation; + } + + /// + /// Gets the sequence number of the animation chunk, starting from 0 + /// + public int SequenceNumber { get; } + + /// + /// Gets the width of the following frame + /// + public int Width { get; } + + /// + /// Gets the height of the following frame + /// + public int Height { get; } + + /// + /// Gets the X position at which to render the following frame + /// + public int XOffset { get; } + + /// + /// Gets the Y position at which to render the following frame + /// + public int YOffset { get; } + + /// + /// Gets the frame delay fraction numerator + /// + public short DelayNumber { get; } + + /// + /// Gets the frame delay fraction denominator + /// + public short DelayDenominator { get; } + + /// + /// Gets the type of frame area disposal to be done after rendering this frame + /// + public APngDisposeOperation DisposeOperation { get; } + + /// + /// Gets the type of frame area rendering for this frame + /// + public APngBlendOperation BlendOperation { get; } + + /// + /// Validates the APng fcTL. + /// + /// + /// Thrown if the image does pass validation. + /// + public void Validate(PngHeader hdr) + { + if (this.XOffset < 0) + { + throw new NotSupportedException($"Invalid XOffset. Expected >= 0. Was '{this.XOffset}'."); + } + + if (this.YOffset < 0) + { + throw new NotSupportedException($"Invalid YOffset. Expected >= 0. Was '{this.YOffset}'."); + } + + if (this.Width <= 0) + { + throw new NotSupportedException($"Invalid Width. Expected > 0. Was '{this.Width}'."); + } + + if (this.Height <= 0) + { + throw new NotSupportedException($"Invalid Height. Expected > 0. Was '{this.Height}'."); + } + + if (this.XOffset + this.Width > hdr.Width) + { + throw new NotSupportedException($"Invalid XOffset or Width. The sum > PngHeader.Width. Was '{this.XOffset + this.Width}'."); + } + + if (this.YOffset + this.Height > hdr.Height) + { + throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'."); + } + } + + /// + /// Writes the fcTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width); + BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); + BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); + BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); + BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber); + BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator); + + buffer[24] = (byte)this.DisposeOperation; + buffer[25] = (byte)this.BlendOperation; + } + + /// + /// Parses the APngFrameControl from the given data buffer. + /// + /// The data to parse. + /// The parsed fcTL. + public static APngFrameControl Parse(ReadOnlySpan data) + => new( + sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]), + height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]), + xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]), + yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]), + delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]), + delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]), + disposeOperation: (APngDisposeOperation)data[24], + blendOperation: (APngBlendOperation)data[25]); +} diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs similarity index 98% rename from src/ImageSharp/Formats/Png/PngHeader.cs rename to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs index 06fec86f30..77fb706f60 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; -namespace SixLabors.ImageSharp.Formats.Png; +namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// Represents the png header chunk. diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs similarity index 89% rename from src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs rename to src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs index 34d53f00eb..7847882484 100644 --- a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers.Binary; @@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// -internal readonly struct PhysicalChunkData +internal readonly struct PngPhysical { public const int Size = 9; - public PhysicalChunkData(uint x, uint y, byte unitSpecifier) + public PngPhysical(uint x, uint y, byte unitSpecifier) { this.XAxisPixelsPerUnit = x; this.YAxisPixelsPerUnit = y; @@ -44,13 +44,13 @@ 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..0ae180e08d 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; @@ -14,7 +15,23 @@ public static partial class MetadataExtensions /// /// Gets the png format specific metadata for the image. /// - /// The metadata this method extends. + /// The metadata this method extends. /// The . - public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance); + public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The . + public static APngFrameMetadata GetAPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The metadata. + /// The . + public static bool TryGetAPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out APngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); + } diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index b514011eb3..e5fa5fbb72 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -42,7 +42,8 @@ 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..2c835bf8ca 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -8,16 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal enum PngChunkType : uint { + /// + /// + /// acTL + AnimationControl = 0x6163544cU, + + /// + /// + /// fcTL + FrameControl = 0x6663544cU, + + /// + /// + /// fdAT + FrameData = 0x66644154U, + /// /// The IDAT chunk contains the actual image data. The image can contains more /// than one chunk of this type. All chunks together are the whole image. /// + /// IDAT Data = 0x49444154U, /// /// This chunk must appear last. It marks the end of the PNG data stream. /// The chunk's data field is empty. /// + /// IEND End = 0x49454E44U, /// @@ -25,34 +42,40 @@ internal enum PngChunkType : uint /// common information like the width and the height of the image or /// the used compression method. /// + /// IHDR Header = 0x49484452U, /// /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// series in the RGB format. /// + /// PLTE Palette = 0x504C5445U, /// /// The eXIf data chunk which contains the Exif profile. /// + /// eXIF Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// + /// gAMA Gamma = 0x67414D41U, /// - /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. + /// This chunk specifies the intended pixel size or aspect ratio for display of the image. /// + /// pHYs Physical = 0x70485973U, /// /// Textual information that the encoder wishes to record with the image can be stored in /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. /// + /// tEXT Text = 0x74455874U, /// @@ -60,70 +83,82 @@ internal enum PngChunkType : uint /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and /// a compressed text string. /// + /// zTXt CompressedText = 0x7A545874U, /// - /// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword + /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword /// and the actual text string, which can be compressed or uncompressed. /// + /// iTXt InternationalText = 0x69545874U, /// - /// The tRNS chunk specifies that the image uses simple transparency: + /// This chunk specifies that the image uses simple transparency: /// either alpha values associated with palette entries (for indexed-color images) /// or a single transparent color (for grayscale and true color images). /// + /// tRNS Transparency = 0x74524E53U, /// - /// The tIME chunk gives the time of the last image modification (not the time of initial image creation). + /// This chunk gives the time of the last image modification (not the time of initial image creation). /// + /// tIME Time = 0x74494d45, /// - /// The bKGD chunk specifies a default background colour to present the image against. + /// This chunk specifies a default background colour to present the image against. /// If there is any other preferred background, either user-specified or part of a larger page (as in a browser), /// the bKGD chunk should be ignored. /// + /// bKGD Background = 0x624b4744, /// - /// The iCCP chunk contains a embedded color profile. If the iCCP chunk is present, + /// This chunk contains a embedded color profile. If the iCCP chunk is present, /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium. /// + /// iCCP EmbeddedColorProfile = 0x69434350, /// - /// The sBIT chunk defines the original number of significant bits (which can be less than or equal to the sample depth). + /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth). /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG. /// + /// sBIT SignificantBits = 0x73424954, /// - /// If the sRGB chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed + /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed /// using the specified rendering intent defined by the International Color Consortium. /// + /// sRGB StandardRgbColourSpace = 0x73524742, /// - /// The hIST chunk gives the approximate usage frequency of each colour in the palette. + /// This chunk gives the approximate usage frequency of each colour in the palette. /// + /// hIST Histogram = 0x68495354, /// - /// The sPLT chunk contains the suggested palette. + /// This chunk contains the suggested palette. /// + /// sPLT SuggestedPalette = 0x73504c54, /// - /// The cHRM chunk may be used to specify the 1931 CIE x,y chromaticities of the red, + /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red, /// green, and blue display primaries used in the image, and the referenced white point. /// + /// cHRM Chroma = 0x6348524d, /// /// Malformed chunk named CgBI produced by apple, which is not conform to the specification. /// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410 /// + /// CgBI ProprietaryApple = 0x43674249 } diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index b76c73b9f2..7877f84bd8 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -28,12 +28,12 @@ internal static class PngConstants /// /// The list of mimetypes that equate to a Png. /// - public static readonly IEnumerable MimeTypes = new[] { "image/png" }; + public static readonly IEnumerable MimeTypes = new[] { "image/png", "image/apng" }; /// /// The list of file extensions that equate to a Png. /// - public static readonly IEnumerable FileExtensions = new[] { "png" }; + public static readonly IEnumerable FileExtensions = new[] { "png", "apng" }; /// /// The header bytes as a big-endian coded ulong. @@ -43,7 +43,7 @@ internal static class PngConstants /// /// The dictionary of available color types. /// - public static readonly Dictionary ColorTypes = new Dictionary + public static readonly Dictionary ColorTypes = new() { [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Rgb] = new byte[] { 8, 16 }, @@ -80,24 +80,5 @@ internal static class PngConstants /// /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// - public static ReadOnlySpan XmpKeyword => new byte[] - { - (byte)'X', - (byte)'M', - (byte)'L', - (byte)':', - (byte)'c', - (byte)'o', - (byte)'m', - (byte)'.', - (byte)'a', - (byte)'d', - (byte)'o', - (byte)'b', - (byte)'e', - (byte)'.', - (byte)'x', - (byte)'m', - (byte)'p' - }; + public static ReadOnlySpan XmpKeyword => "XML:com.adobe.xmp"u8; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index d1d29dca6b..fa94e6925c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Compression; using System.Runtime.CompilerServices; @@ -28,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal sealed class PngDecoderCore : IImageDecoderInternals { + /// + /// Indicate whether the file is a simple PNG. + /// + private bool isSimplePng; + /// /// The general decoder options. /// @@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private BufferedReadStream currentStream; + private BufferedReadStream currentStream = null!; /// /// The png header. /// private PngHeader header; + /// + /// The png animation control. + /// + private APngAnimationControl? animationControl; + /// /// The number of bytes per pixel. /// @@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The palette containing color information for indexed png's. /// - private byte[] palette; + private byte[] palette = null!; /// /// The palette containing alpha channel color information for indexed png's. /// - private byte[] paletteAlpha; + private byte[] paletteAlpha = null!; /// /// Previous scanline processed. /// - private IMemoryOwner previousScanline; + private IMemoryOwner previousScanline = null!; /// /// The current scanline that is being processed. /// - private IMemoryOwner scanline; - - /// - /// The index of the current scanline being processed. - /// - private int currentRow = Adam7.FirstRow[0]; - - /// - /// The current number of bytes read in the current scanline. - /// - private int currentRowBytesRead; + private IMemoryOwner scanline = null!; /// /// Gets or sets the png color type. @@ -148,7 +148,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; this.currentStream.Skip(8); - Image image = null; + Image? image = null; + APngFrameControl? lastFrameControl = null; + ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; try @@ -160,22 +162,84 @@ 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: + if (this.isSimplePng || this.animationControl is not null) + { + PngThrowHelper.ThrowInvalidAnimationControl(); + } + + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Gamma: ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; - case PngChunkType.Data: + case PngChunkType.FrameControl: + if (this.isSimplePng) + { + continue; + } + + currentFrame = null; + lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: if (image is null) { - this.InitializeImage(metadata, out image); + PngThrowHelper.ThrowMissingDefaultData(); } - this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken); + if (lastFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + if (currentFrame is null) + { + this.InitializeFrame(lastFrameControl.Value, image, out currentFrame); + } + + this.currentStream.Position += 4; + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + () => + { + int length = this.ReadNextDataChunk(); + if (this.ReadNextDataChunk() is 0) + { + return length; + } + + this.currentStream.Position += 4; // Skip sequence number + return length - 4; + }, + cancellationToken); + lastFrameControl = null; + break; + case PngChunkType.Data: + if (this.animationControl is null) + { + this.isSimplePng = true; + } + + if (image is null) + { + this.InitializeImage(metadata, lastFrameControl, out image); + } + this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, cancellationToken); + lastFrameControl = null; break; case PngChunkType.Palette: byte[] pal = new byte[chunk.Length]; @@ -249,6 +313,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; + APngFrameControl? lastFrameControl = null; Span buffer = stackalloc byte[20]; this.currentStream.Skip(8); @@ -264,6 +329,14 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.AnimationControl: + if (this.isSimplePng || this.animationControl is not null) + { + PngThrowHelper.ThrowInvalidAnimationControl(); + } + + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: if (this.colorMetadataOnly) { @@ -282,7 +355,34 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.FrameControl: + if (this.isSimplePng) + { + continue; + } + + lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: + if (this.colorMetadataOnly) + { + goto EOF; + } + + if (lastFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + // Skip sequence number + this.currentStream.Skip(4); + this.SkipChunkDataAndCrc(chunk); + break; case PngChunkType.Data: + if (this.animationControl is null) + { + this.isSimplePng = true; + } // Spec says tRNS must be before IDAT so safe to exit. if (this.colorMetadataOnly) @@ -365,9 +465,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat } EOF: - if (this.header.Width == 0 && this.header.Height == 0) + if (this.header is { Width: 0, Height: 0 }) { - PngThrowHelper.ThrowNoHeader(); + PngThrowHelper.ThrowInvalidHeader(); } return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); @@ -398,7 +498,7 @@ 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) { @@ -433,7 +533,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 @@ -466,8 +566,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, APngFrameControl? frameControl, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( @@ -476,6 +577,12 @@ private void InitializeImage(ImageMetadata metadata, out Image i this.header.Height, metadata); + if (frameControl is { } control) + { + APngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAPngFrameMetadata(); + frameMetadata.FromChunk(control); + } + this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; @@ -490,6 +597,27 @@ 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 frame + /// The image that we will populate + private void InitializeFrame(APngFrameControl frameControl, Image image, out ImageFrame frame) + where TPixel : unmanaged, IPixel + { + frame = image.Frames.CreateFrame(); + + APngFrameMetadata frameMetadata = frame.Metadata.GetAPngFrameMetadata(); + + frameMetadata.FromChunk(frameControl); + + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + } + /// /// Calculates the correct number of bits per pixel for the given color type. /// @@ -553,18 +681,19 @@ 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 cancellation token. - private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk); - deframeStream.AllocateNewBytes(chunk.Length, true); - DeflateStream dataStream = deframeStream.CompressedStream; + using ZlibInflateStream deframeStream = new(this.currentStream, getData); + deframeStream.AllocateNewBytes(chunkLength, true); + DeflateStream dataStream = deframeStream.CompressedStream!; - if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) + if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken); } @@ -585,22 +714,25 @@ private void ReadScanlines(PngChunk chunk, ImageFrame image, Png private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - while (this.currentRow < this.header.Height) + int currentRow = Adam7.FirstRow[0]; + int currentRowBytesRead = 0; + int height = image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height; + while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); Span scanlineSpan = this.scanline.GetSpan(); - while (this.currentRowBytesRead < this.bytesPerScanline) + while (currentRowBytesRead < this.bytesPerScanline) { - int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, this.bytesPerScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; switch ((FilterType)scanlineSpan[0]) { @@ -628,10 +760,10 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< break; } - this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); + this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); - this.currentRow++; + ++currentRow; } } @@ -647,8 +779,17 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + int currentRow = Adam7.FirstRow[0]; + int currentRowBytesRead = 0; int pass = 0; int width = this.header.Width; + int height = this.header.Height; + if (image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata)) + { + width = frameMetadata.Width; + height = frameMetadata.Height; + } + Buffer2D imageBuffer = image.PixelBuffer; while (true) { @@ -656,7 +797,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I if (numColumns == 0) { - pass++; + ++pass; // This pass contains no data; skip to next pass continue; @@ -664,21 +805,21 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (this.currentRow < this.header.Height) + while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); - while (this.currentRowBytesRead < bytesPerInterlaceScanline) + while (currentRowBytesRead < bytesPerInterlaceScanline) { - int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); @@ -709,12 +850,12 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I break; } - Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow); + Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); - this.currentRow += Adam7.RowIncrement[pass]; + currentRow += Adam7.RowIncrement[pass]; } pass++; @@ -722,7 +863,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I if (pass < 7) { - this.currentRow = Adam7.FirstRow[pass]; + currentRow = Adam7.FirstRow[pass]; } else { @@ -736,19 +877,20 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I /// Processes the de-filtered scanline filling the image pixel data /// /// The pixel format. + /// The index of the current scanline being processed. /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline(int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { - Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow); + Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( @@ -840,7 +982,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi ReadOnlySpan trimmed = defilteredScanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( @@ -975,6 +1117,31 @@ 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 = APngAnimationControl.Parse(data); + + pngMetadata.NumberPlays = this.animationControl.NumberPlays; + } + + /// + /// Reads a header chunk from the data. + /// + /// The containing data. + private APngFrameControl ReadFrameControlChunk(ReadOnlySpan data) + { + APngFrameControl fcTL = APngFrameControl.Parse(data); + + fcTL.Validate(this.header); + + return fcTL; + } + /// /// Reads a header chunk from the data. /// @@ -1062,7 +1229,7 @@ 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)); @@ -1355,7 +1522,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)); } @@ -1378,7 +1545,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)) { @@ -1407,7 +1574,7 @@ private int ReadNextDataChunk() if (this.TryReadChunk(buffer, out PngChunk chunk)) { - if (chunk.Type == PngChunkType.Data) + if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData) { chunk.Data?.Dispose(); return chunk.Length; @@ -1461,7 +1628,7 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. - if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency) + if (this.colorMetadataOnly && type is not PngChunkType.Header and not PngChunkType.Transparency) { chunk = new PngChunk(length, type); @@ -1476,9 +1643,9 @@ 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/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 175a9f777d..8fcd1721d3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Memory; @@ -647,9 +648,9 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta) return; } - PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); + PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size); + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size); } /// diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 2d1f2dcc7d..292f087f27 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// -public sealed class PngFormat : IImageFormat +public sealed class PngFormat : IImageFormat { private PngFormat() { @@ -31,4 +31,7 @@ private PngFormat() /// public PngMetadata CreateDefaultFormatMetadata() => new(); + + /// + public APngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9ff3905fe1..9f874d5c9a 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -32,6 +34,7 @@ private PngMetadata(PngMetadata other) this.TransparentL16 = other.TransparentL16; this.TransparentRgb24 = other.TransparentRgb24; this.TransparentRgb48 = other.TransparentRgb48; + this.NumberPlays = other.NumberPlays; for (int i = 0; i < other.TextData.Count; i++) { @@ -95,6 +98,11 @@ 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 NumberPlays { get; set; } + /// public IDeepCloneable DeepClone() => new PngMetadata(this); } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 04a23308cc..caba887921 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 67da78e45b..78c243eeef 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -12,13 +12,22 @@ 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 +39,7 @@ 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 ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type."); [DoesNotReturn] public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 75d4b173c8..57608a9090 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,6 +13,7 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release + preview diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index d7a353665a..ff81401f56 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -2,7 +2,9 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -132,6 +134,34 @@ public void Decode_ReadsExifData(TestImageProvider provider) VerifyExifDataIsPresent(exif); } + [Theory] + [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)] + public void Decode_ReadsExifData2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + SkipMetadata = false + }; + + using Image image = provider.GetImage(PngDecoder.Instance, options); + TPixel pixel = image.Frames.RootFrame[5, 5]; + TPixel pixel2 = image.Frames[1][5, 5]; + } + + [Theory] + [WithFile(@"Png\pl.png", PixelTypes.Rgba32)] + public void Decode_ReadsExifData3(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + SkipMetadata = false + }; + + using Image image = provider.GetImage(PngDecoder.Instance, options); + } + [Theory] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 04341a2419..96b5b620b8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png.Chunks; namespace SixLabors.ImageSharp.Tests.Formats.Png; From 7b6c32d54b03351853f1605b18280e93cac4d73b Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 12 Aug 2023 12:47:34 +0800 Subject: [PATCH 02/20] implement APNG encoder --- src/ImageSharp/Configuration.cs | 2 +- .../Formats/Png/Chunks/APngFrameControl.cs | 24 +- src/ImageSharp/Formats/Png/PngChunkType.cs | 44 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 8 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 410 ++++++++++-------- src/ImageSharp/Formats/Png/PngMetadata.cs | 1 - .../Formats/Png/PngDecoderTests.cs | 22 +- .../Formats/Png/PngMetadataTests.cs | 29 -- .../Formats/Png/PngTextDataTests.cs | 1 - tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/apng.png | 3 + 11 files changed, 311 insertions(+), 234 deletions(-) create mode 100644 tests/Images/Input/Png/apng.png diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 39fcef9c40..7692238be1 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -43,7 +43,7 @@ public Configuration() /// Initializes a new instance of the class. /// /// A collection of configuration modules to register. - public Configuration(params IImageFormatConfigurationModule[] configurationModules) + public Configuration(params IImageFormatConfigurationModule[]? configurationModules) { if (configurationModules != null) { diff --git a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs index e239bd8e2e..ac9d1e5602 100644 --- a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs @@ -115,6 +115,26 @@ public void Validate(PngHeader hdr) } } + /// + /// Parses the APngFrameControl from the given metadata. + /// + /// The metadata to parse. + /// Sequence number. + public static APngFrameControl FromMetadata(APngFrameMetadata frameMetadata, int sequenceNumber) + { + APngFrameControl fcTL = new( + sequenceNumber, + frameMetadata.Width, + frameMetadata.Height, + frameMetadata.XOffset, + frameMetadata.YOffset, + frameMetadata.DelayNumber, + frameMetadata.DelayDenominator, + frameMetadata.DisposeOperation, + frameMetadata.BlendOperation); + return fcTL; + } + /// /// Writes the fcTL to the given buffer. /// @@ -126,8 +146,8 @@ public void WriteTo(Span buffer) BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); - BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber); - BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator); + BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber); + BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator); buffer[24] = (byte)this.DisposeOperation; buffer[25] = (byte)this.BlendOperation; diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 2c835bf8ca..866bf28af1 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -10,31 +10,31 @@ internal enum PngChunkType : uint { /// /// - /// acTL + /// acTL (Single) AnimationControl = 0x6163544cU, /// /// - /// fcTL + /// fcTL (Multiple) FrameControl = 0x6663544cU, /// /// - /// fdAT + /// fdAT (Multiple) FrameData = 0x66644154U, /// /// The IDAT chunk contains the actual image data. The image can contains more /// than one chunk of this type. All chunks together are the whole image. /// - /// IDAT + /// 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 + /// IEND (Single) End = 0x49454E44U, /// @@ -42,40 +42,40 @@ internal enum PngChunkType : uint /// common information like the width and the height of the image or /// the used compression method. /// - /// IHDR + /// IHDR (Single) Header = 0x49484452U, /// /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// series in the RGB format. /// - /// PLTE + /// PLTE (Single) Palette = 0x504C5445U, /// /// The eXIf data chunk which contains the Exif profile. /// - /// eXIF + /// eXIF (Single) Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// - /// gAMA + /// gAMA (Single) Gamma = 0x67414D41U, /// /// This chunk specifies the intended pixel size or aspect ratio for display of the image. /// - /// pHYs + /// 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 + /// tEXT (Multiple) Text = 0x74455874U, /// @@ -83,14 +83,14 @@ 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 + /// zTXt (Multiple) CompressedText = 0x7A545874U, /// /// 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 + /// iTXt (Multiple) InternationalText = 0x69545874U, /// @@ -98,13 +98,13 @@ internal enum PngChunkType : uint /// either alpha values associated with palette entries (for indexed-color images) /// or a single transparent color (for grayscale and true color images). /// - /// tRNS + /// tRNS (Single) Transparency = 0x74524E53U, /// /// This chunk gives the time of the last image modification (not the time of initial image creation). /// - /// tIME + /// tIME (Single) Time = 0x74494d45, /// @@ -112,47 +112,47 @@ internal enum PngChunkType : uint /// 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 + /// bKGD (Single) Background = 0x624b4744, /// /// 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 + /// iCCP (Single) EmbeddedColorProfile = 0x69434350, /// /// 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 + /// sBIT (Single) SignificantBits = 0x73424954, /// /// 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 + /// sRGB (Single) StandardRgbColourSpace = 0x73524742, /// /// This chunk gives the approximate usage frequency of each colour in the palette. /// - /// hIST + /// hIST (Single) Histogram = 0x68495354, /// /// This chunk contains the suggested palette. /// - /// sPLT + /// sPLT (Single) SuggestedPalette = 0x73504c54, /// /// 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 + /// cHRM (Single) Chroma = 0x6348524d, /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 1d068303bc..bf8b23b8f0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -1,6 +1,5 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using SixLabors.ImageSharp.Advanced; @@ -18,7 +17,12 @@ public PngEncoder() => // We set the quantizer to null here to allow the underlying encoder to create a // quantizer with options appropriate to the encoding bit depth. - this.Quantizer = null; + this.Quantizer = null!; + + /// + /// Gets whether the file is a simple PNG. + /// + public bool? IsSimplePng { get; init; } /// /// Gets the number of bits per sample or per palette index (not per pixel). diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8fcd1721d3..2cbc86f420 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; @@ -9,7 +8,6 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Memory; @@ -27,7 +25,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The maximum block size, defaults at 64k for uncompressed blocks. /// - private const int MaxBlockSize = 65535; + private const int MaxBlockSize = (1 << 16) - 1; /// /// Used the manage memory allocations. @@ -102,12 +100,12 @@ 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. @@ -147,34 +145,59 @@ public void Encode(Image image, Stream stream, CancellationToken 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; + Image? clonedImage = null; + Image targetImage = image; + bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; if (clearTransparency) { - clonedImage = image.Clone(); - ClearTransparentPixels(clonedImage); + targetImage = clonedImage = image.Clone(); + ClearTransparentPixels(targetImage); } - IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage); + IndexedImageFrame? rootQuantized = this.CreateQuantizedImageAndUpdateBitDepth(targetImage.Frames.RootFrame); stream.Write(PngConstants.HeaderBytes); this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); this.WriteColorProfileChunk(stream, metadata); - this.WritePaletteChunk(stream, quantized); + this.WritePaletteChunk(stream, rootQuantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream); + + if (this.encoder.IsSimplePng is not true && targetImage.Frames.Count > 1) + { + this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); + + this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetAPngFrameMetadata(), 0); + _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false); + + int index = 1; + + foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1)) + { + this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetAPngFrameMetadata(), index); + ++index; + IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame); + index += this.WriteDataChunks(imageFrame, quantized, stream, true, index); + quantized?.Dispose(); + } + } + else + { + _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false); + rootQuantized?.Dispose(); + } + this.WriteEndChunk(stream); stream.Flush(); - quantized?.Dispose(); clonedImage?.Dispose(); + rootQuantized?.Dispose(); } /// @@ -182,8 +205,8 @@ public void Dispose() { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); - this.previousScanline = null; - this.currentScanline = null; + this.previousScanline = null!; + this.currentScanline = null!; } /// @@ -192,48 +215,44 @@ public void Dispose() /// 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 => + where TPixel : unmanaged, IPixel + { + foreach (ImageFrame imageFrame in image.Frames) { - // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 rgba32 = default; - Rgba32 transparent = Color.Transparent; - for (int y = 0; y < accessor.Height; y++) + imageFrame.ProcessPixelRows(accessor => { - Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) + // TODO: We should be able to speed this up with SIMD and masking. + Rgba32 rgba32 = default; + Rgba32 transparent = Color.Transparent; + for (int y = 0; y < accessor.Height; ++y) { - span[x].ToRgba32(ref rgba32); - - if (rgba32.A == 0) + Span span = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; ++x) { - span[x].FromRgba32(transparent); + span[x].ToRgba32(ref rgba32); + + if (rgba32.A is 0) + { + span[x].FromRgba32(transparent); + } } } - } - }); + }); + } + + } /// /// 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 frame to quantize. /// The quantized image. - private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth( - Image image, - Image clonedImage) + private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( + ImageFrame frame) 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 = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, frame); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; @@ -245,9 +264,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) { @@ -260,7 +277,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); // Can't map directly to byte array as it's big-endian. - for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) + for (int x = 0, o = 0; x < luminanceSpan.Length; ++x, o += 2) { L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); @@ -300,7 +317,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan); // Can't map directly to byte array as it's big endian. - for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4) + for (int x = 0, o = 0; x < laSpan.Length; ++x, o += 4) { La32 la = Unsafe.Add(ref laRef, (uint)x); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L); @@ -403,20 +420,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; @@ -477,7 +493,7 @@ private void CollectAndFilterPixelRow( ReadOnlySpan rowSpan, ref Span filter, ref Span attempt, - IndexedImageFrame quantized, + IndexedImageFrame? quantized, int row) where TPixel : unmanaged, IPixel { @@ -577,6 +593,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) + { + APngAnimationControl acTL = new(framesCount, playsCount); + + acTL.WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, APngAnimationControl.Size); + } + /// /// Writes the palette chunk to the stream. /// Should be written before the first IDAT chunk. @@ -584,7 +615,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) @@ -692,9 +723,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; } @@ -761,18 +792,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)) { @@ -876,7 +898,7 @@ private void WriteGammaChunk(Stream stream) // 4-byte unsigned integer of gamma * 100,000. uint gammaValue = (uint)(this.gamma * 100_000F); - BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue); + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue); this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); } @@ -896,51 +918,69 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) } Span alpha = this.chunkDataBuffer.Span; - if (pngMetadata.ColorType == PngColorType.Rgb) + switch (pngMetadata.ColorType) { - if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) - { - Rgb48 rgb = pngMetadata.TransparentRgb48.Value; - BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); - BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); - BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); + case PngColorType.Rgb when pngMetadata.TransparentRgb48.HasValue && this.use16Bit: + Rgb48 rgb48 = pngMetadata.TransparentRgb48.Value; + BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb48.R); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb48.G); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb48.B); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); - } - else if (pngMetadata.TransparentRgb24.HasValue) - { - alpha.Clear(); - Rgb24 rgb = pngMetadata.TransparentRgb24.Value; - alpha[1] = rgb.R; - alpha[3] = rgb.G; - alpha[5] = rgb.B; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); - } - } - else if (pngMetadata.ColorType == PngColorType.Grayscale) - { - if (pngMetadata.TransparentL16.HasValue && this.use16Bit) - { + break; + case PngColorType.Rgb: + if (pngMetadata.TransparentRgb24.HasValue) + { + alpha.Clear(); + Rgb24 rgb24 = pngMetadata.TransparentRgb24.Value; + alpha[1] = rgb24.R; + alpha[3] = rgb24.G; + alpha[5] = rgb24.B; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + } + + break; + case PngColorType.Grayscale when pngMetadata.TransparentL16.HasValue && this.use16Bit: BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); - } - else if (pngMetadata.TransparentL8.HasValue) - { - alpha.Clear(); - alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); - } + break; + case PngColorType.Grayscale: + if (pngMetadata.TransparentL8.HasValue) + { + alpha.Clear(); + alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + } + + break; } } + /// + /// Writes the animation control chunk to the stream. + /// + /// The containing image data. + /// Provides APng specific metadata information for the image frame. + /// Sequence number. + private void WriteFrameControlChunk(Stream stream, APngFrameMetadata frameMetadata, int sequenceNumber) + { + APngFrameControl fcTL = APngFrameControl.FromMetadata(frameMetadata, sequenceNumber); + + fcTL.WriteTo(this.chunkDataBuffer.Span); + + this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, APngFrameControl.Size); + } + /// /// Writes the pixel information to the stream. /// /// The pixel format. - /// The image. + /// 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. + /// Start sequence number. + private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame, int startSequenceNumber = 0) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -950,9 +990,9 @@ 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) + { + byte[] chunkBuffer = new byte[MaxBlockSize]; + BinaryPrimitives.WriteInt32BigEndian(chunkBuffer, startSequenceNumber + i); + buffer.AsSpan().Slice(i * maxBlockSize, length).CopyTo(chunkBuffer.AsSpan(4, length)); + + this.WriteChunk(stream, PngChunkType.FrameData, chunkBuffer, 0, length + 4); + } + else + { + this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length); + } } + + return numChunks; } /// @@ -1013,10 +1072,18 @@ private void AllocateScanlineBuffers(int bytesPerScanline) /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int bytesPerScanline = this.CalculateScanlineLength(this.width); + int width = this.width; + int height = this.height; + if (pixels.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? pngMetadata)) + { + width = pngMetadata.Width; + height = pngMetadata.Height; + } + + int bytesPerScanline = this.CalculateScanlineLength(width); int filterLength = bytesPerScanline + 1; this.AllocateScanlineBuffers(bytesPerScanline); @@ -1027,7 +1094,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 = 0; y < height; ++y) { this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); deflateStream.Write(filter); @@ -1040,14 +1107,14 @@ private void EncodePixels(Image pixels, IndexedImageFrame /// The type of the pixel. - /// The image. + /// The image frame. /// The deflate stream. - private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(ImageFrame frame, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer; + int width = frame.Width; + int height = frame.Height; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int pass = 0; pass < 7; pass++) { int startRow = Adam7.FirstRow[pass]; @@ -1132,7 +1199,8 @@ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantize col < width; col += Adam7.ColumnIncrement[pass]) { - block[i++] = srcRow[col]; + block[i] = srcRow[col]; + ++i; } // Encode data @@ -1176,7 +1244,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o stream.Write(buffer); - uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer + uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer if (data.Length > 0 && length > 0) { @@ -1199,7 +1267,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; @@ -1243,14 +1311,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. @@ -1266,7 +1327,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; } @@ -1277,28 +1338,29 @@ private void SanitizeAndSetEncoderOptions( /// The png encoder. /// The color type. /// The bits per component. - /// The image. - private static IndexedImageFrame CreateQuantizedFrame( + /// The frame. + private static IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, PngColorType colorType, byte bitDepth, - Image image) + ImageFrame frame) where TPixel : unmanaged, IPixel { - if (colorType != PngColorType.Palette) + if (colorType is not PngColorType.Palette) { return null; } // Use the metadata to determine what quantization depth to use if no quantizer has been set. + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract IQuantizer quantizer = encoder.Quantizer - ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + ?? 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.GetConfiguration()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(frame.GetConfiguration()); - frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); - return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); + return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } /// @@ -1312,25 +1374,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; } @@ -1368,21 +1428,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 }; @@ -1393,27 +1453,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/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9f874d5c9a..c4d136505f 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2dfd99439a..2fc0dc0c06 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; @@ -107,6 +106,27 @@ public void Decode(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void Decode_APng(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + image.SaveAsPng("C:\\WorkSpace\\1.png"); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + // TODO test + } + + [Theory] + [WithFile("C:\\WorkSpace\\Fuck.png", PixelTypes.Rgba32)] + public void Decode_APng2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + image.SaveAsPng("C:\\WorkSpace\\1.png"); + } + [Theory] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] public void PngDecoder_Decode_Resize(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index ff81401f56..4492934f1a 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Metadata; @@ -134,34 +133,6 @@ public void Decode_ReadsExifData(TestImageProvider provider) VerifyExifDataIsPresent(exif); } - [Theory] - [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)] - public void Decode_ReadsExifData2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - DecoderOptions options = new() - { - SkipMetadata = false - }; - - using Image image = provider.GetImage(PngDecoder.Instance, options); - TPixel pixel = image.Frames.RootFrame[5, 5]; - TPixel pixel2 = image.Frames[1][5, 5]; - } - - [Theory] - [WithFile(@"Png\pl.png", PixelTypes.Rgba32)] - public void Decode_ReadsExifData3(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - DecoderOptions options = new() - { - SkipMetadata = false - }; - - using Image image = provider.GetImage(PngDecoder.Instance, options); - } - [Theory] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 96b5b620b8..878f3fb8d4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Gif; 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 a25424b6d9..2ddcc559b8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -61,6 +61,7 @@ public static class Png public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string XmpColorPalette = "Png/xmp-colorpalette.png"; + public const string APng = "Png/apng.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/apng.png b/tests/Images/Input/Png/apng.png new file mode 100644 index 0000000000..c5b2adf8e9 --- /dev/null +++ b/tests/Images/Input/Png/apng.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6b0e5a904e269a9108b32c0f5cc98cda4240a60db421f560f45d2e36ead32a9 +size 212 From 01caebd34a9c5e950c1c2edeceeb2bcb3ea36ab6 Mon Sep 17 00:00:00 2001 From: Poker Date: Sun, 13 Aug 2023 11:55:43 +0800 Subject: [PATCH 03/20] Add UnitTest --- src/ImageSharp/Formats/Png/PngChunkType.cs | 38 +++++++++++-------- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 7 ++-- .../Formats/Png/PngDecoderTests.cs | 15 +------- .../Formats/Png/PngEncoderTests.cs | 13 +++++++ .../TestUtilities/ImagingTestCaseUtility.cs | 14 +++---- .../TestUtilities/TestImageExtensions.cs | 12 ++---- tests/Images/Input/Png/apng.png | 4 +- 7 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 866bf28af1..a008bf8ea2 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -9,22 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Png; internal enum PngChunkType : uint { /// - /// - /// acTL (Single) - AnimationControl = 0x6163544cU, - - /// - /// - /// fcTL (Multiple) - FrameControl = 0x6663544cU, - - /// - /// - /// fdAT (Multiple) - FrameData = 0x66644154U, - - /// - /// 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) @@ -155,6 +140,27 @@ internal enum PngChunkType : uint /// 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 diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 2cbc86f420..1550417890 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -168,7 +168,8 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - if (this.encoder.IsSimplePng is not true && targetImage.Frames.Count > 1) + if ((this.encoder.IsSimplePng is null && targetImage.Frames.Count > 1) + || this.encoder.IsSimplePng is false) { this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); @@ -642,7 +643,7 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame? ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); // Loop, assign, and extract alpha values from the palette. - for (int i = 0; i < paletteLength; i++) + for (int i = 0; i < paletteLength; ++i) { Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, (uint)i); byte alpha = rgba.A; @@ -674,7 +675,7 @@ 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; } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2fc0dc0c06..19283ebf88 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -111,20 +111,7 @@ public void Decode(TestImageProvider provider) public void Decode_APng(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(PngDecoder.Instance); - image.SaveAsPng("C:\\WorkSpace\\1.png"); - image.DebugSave(provider); - image.CompareToOriginal(provider, ImageComparer.Exact); - // TODO test - } - - [Theory] - [WithFile("C:\\WorkSpace\\Fuck.png", PixelTypes.Rgba32)] - public void Decode_APng2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(PngDecoder.Instance); - image.SaveAsPng("C:\\WorkSpace\\1.png"); + using Image image = provider.GetImage(PngDecoder.Instance); // MagickReferenceDecoder cannot decode APNGs } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b4fda5d32f..74885283de 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -439,6 +439,19 @@ 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; + using Image output = Image.Load(memStream); + ImageComparer.Exact.VerifySimilarity(output, image); + } + [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 460ecac85a..42be466455 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -208,7 +208,7 @@ public string[] SaveTestOutputFileMultiFrame( bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel { - encoder = encoder ?? TestEnvironment.GetReferenceEncoder($"foo.{extension}"); + encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}"); string[] files = this.GetTestOutputFileNamesMultiFrame( image.Frames.Count, @@ -218,14 +218,10 @@ public string[] SaveTestOutputFileMultiFrame( for (int i = 0; i < image.Frames.Count; i++) { - using (Image frameImage = image.Frames.CloneFrame(i)) - { - string filePath = files[i]; - using (FileStream stream = File.OpenWrite(filePath)) - { - frameImage.Save(stream, encoder); - } - } + using Image frameImage = image.Frames.CloneFrame(i); + string filePath = files[i]; + using FileStream stream = File.OpenWrite(filePath); + frameImage.Save(stream, encoder); } return files; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 31c9f541ea..7cdf66e3a7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -534,10 +534,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; } @@ -560,10 +558,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/Images/Input/Png/apng.png b/tests/Images/Input/Png/apng.png index c5b2adf8e9..7def301ae6 100644 --- a/tests/Images/Input/Png/apng.png +++ b/tests/Images/Input/Png/apng.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6b0e5a904e269a9108b32c0f5cc98cda4240a60db421f560f45d2e36ead32a9 -size 212 +oid sha256:7c15e4670da1826d1cc25555bd6cbe287ecc70327cd029a7613334a39a283021 +size 2508 From 64a0ff08e9f9e051236a7de38c6f34f47c5ca80d Mon Sep 17 00:00:00 2001 From: Poker Date: Thu, 17 Aug 2023 22:27:08 +0800 Subject: [PATCH 04/20] Fix simple issues in review --- src/ImageSharp/Configuration.cs | 2 +- ...nimationControl.cs => AnimationControl.cs} | 4 +- .../{APngFrameControl.cs => FrameControl.cs} | 22 +++---- .../Formats/Png/MetadataExtensions.cs | 9 ++- ...BlendOperation.cs => PngBlendOperation.cs} | 2 +- src/ImageSharp/Formats/Png/PngConstants.cs | 21 +++++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 66 +++++-------------- ...oseOperation.cs => PngDisposeOperation.cs} | 2 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 5 -- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 41 ++++++------ src/ImageSharp/Formats/Png/PngFormat.cs | 4 +- ...ngFrameMetadata.cs => PngFrameMetadata.cs} | 20 +++--- src/ImageSharp/ImageSharp.csproj | 1 - 13 files changed, 88 insertions(+), 111 deletions(-) rename src/ImageSharp/Formats/Png/Chunks/{APngAnimationControl.cs => AnimationControl.cs} (92%) rename src/ImageSharp/Formats/Png/Chunks/{APngFrameControl.cs => FrameControl.cs} (89%) rename src/ImageSharp/Formats/Png/{APngBlendOperation.cs => PngBlendOperation.cs} (96%) rename src/ImageSharp/Formats/Png/{APngDisposeOperation.cs => PngDisposeOperation.cs} (95%) rename src/ImageSharp/Formats/Png/{APngFrameMetadata.cs => PngFrameMetadata.cs} (79%) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 7692238be1..39fcef9c40 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -43,7 +43,7 @@ public Configuration() /// Initializes a new instance of the class. /// /// A collection of configuration modules to register. - public Configuration(params IImageFormatConfigurationModule[]? configurationModules) + public Configuration(params IImageFormatConfigurationModule[] configurationModules) { if (configurationModules != null) { diff --git a/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs similarity index 92% rename from src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs rename to src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs index ca8268cd5d..7a76e5a095 100644 --- a/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; -internal record APngAnimationControl( +internal record AnimationControl( int NumberFrames, int NumberPlays) { @@ -36,7 +36,7 @@ public void WriteTo(Span buffer) /// /// The data to parse. /// The parsed acTL. - public static APngAnimationControl Parse(ReadOnlySpan data) + 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/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs similarity index 89% rename from src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs rename to src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index ac9d1e5602..5f0bc716dc 100644 --- a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -5,11 +5,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; -internal readonly struct APngFrameControl +internal readonly struct FrameControl { public const int Size = 26; - public APngFrameControl( + public FrameControl( int sequenceNumber, int width, int height, @@ -17,8 +17,8 @@ public APngFrameControl( int yOffset, short delayNumber, short delayDenominator, - APngDisposeOperation disposeOperation, - APngBlendOperation blendOperation) + PngDisposeOperation disposeOperation, + PngBlendOperation blendOperation) { this.SequenceNumber = sequenceNumber; this.Width = width; @@ -69,12 +69,12 @@ public APngFrameControl( /// /// Gets the type of frame area disposal to be done after rendering this frame /// - public APngDisposeOperation DisposeOperation { get; } + public PngDisposeOperation DisposeOperation { get; } /// /// Gets the type of frame area rendering for this frame /// - public APngBlendOperation BlendOperation { get; } + public PngBlendOperation BlendOperation { get; } /// /// Validates the APng fcTL. @@ -120,9 +120,9 @@ public void Validate(PngHeader hdr) /// /// The metadata to parse. /// Sequence number. - public static APngFrameControl FromMetadata(APngFrameMetadata frameMetadata, int sequenceNumber) + public static FrameControl FromMetadata(PngFrameMetadata frameMetadata, int sequenceNumber) { - APngFrameControl fcTL = new( + FrameControl fcTL = new( sequenceNumber, frameMetadata.Width, frameMetadata.Height, @@ -158,7 +158,7 @@ public void WriteTo(Span buffer) /// /// The data to parse. /// The parsed fcTL. - public static APngFrameControl Parse(ReadOnlySpan data) + public static FrameControl Parse(ReadOnlySpan data) => new( sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]), width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]), @@ -167,6 +167,6 @@ public static APngFrameControl Parse(ReadOnlySpan data) yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]), delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]), delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]), - disposeOperation: (APngDisposeOperation)data[24], - blendOperation: (APngBlendOperation)data[25]); + disposeOperation: (PngDisposeOperation)data[24], + blendOperation: (PngBlendOperation)data[25]); } diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs index 0ae180e08d..f24b8d1b5c 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -23,15 +23,14 @@ public static partial class MetadataExtensions /// Gets the aPng format specific metadata for the image frame. /// /// The metadata this method extends. - /// The . - public static APngFrameMetadata GetAPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + /// 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 TryGetAPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out APngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out 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/APngBlendOperation.cs b/src/ImageSharp/Formats/Png/PngBlendOperation.cs similarity index 96% rename from src/ImageSharp/Formats/Png/APngBlendOperation.cs rename to src/ImageSharp/Formats/Png/PngBlendOperation.cs index 0e8cdb4289..b8a84a933e 100644 --- a/src/ImageSharp/Formats/Png/APngBlendOperation.cs +++ b/src/ImageSharp/Formats/Png/PngBlendOperation.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer. /// -public enum APngBlendOperation +public enum PngBlendOperation { /// /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index 7877f84bd8..43f2b0fb25 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -80,5 +80,24 @@ internal static class PngConstants /// /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// - public static ReadOnlySpan XmpKeyword => "XML:com.adobe.xmp"u8; + public static ReadOnlySpan XmpKeyword => new[] + { + (byte)'X', + (byte)'M', + (byte)'L', + (byte)':', + (byte)'c', + (byte)'o', + (byte)'m', + (byte)'.', + (byte)'a', + (byte)'d', + (byte)'o', + (byte)'b', + (byte)'e', + (byte)'.', + (byte)'x', + (byte)'m', + (byte)'p' + }; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index fa94e6925c..ac9aa5fad0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -28,11 +28,6 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal sealed class PngDecoderCore : IImageDecoderInternals { - /// - /// Indicate whether the file is a simple PNG. - /// - private bool isSimplePng; - /// /// The general decoder options. /// @@ -66,7 +61,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The png animation control. /// - private APngAnimationControl? animationControl; + private AnimationControl? animationControl; /// /// The number of bytes per pixel. @@ -149,7 +144,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream = stream; this.currentStream.Skip(8); Image? image = null; - APngFrameControl? lastFrameControl = null; + FrameControl? lastFrameControl = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -170,11 +165,6 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.AnimationControl: - if (this.isSimplePng || this.animationControl is not null) - { - PngThrowHelper.ThrowInvalidAnimationControl(); - } - this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Physical: @@ -184,11 +174,6 @@ public Image Decode(BufferedReadStream stream, CancellationToken ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: - if (this.isSimplePng) - { - continue; - } - currentFrame = null; lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; @@ -228,11 +213,6 @@ public Image Decode(BufferedReadStream stream, CancellationToken lastFrameControl = null; break; case PngChunkType.Data: - if (this.animationControl is null) - { - this.isSimplePng = true; - } - if (image is null) { this.InitializeImage(metadata, lastFrameControl, out image); @@ -313,7 +293,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; - APngFrameControl? lastFrameControl = null; + FrameControl? lastFrameControl = null; Span buffer = stackalloc byte[20]; this.currentStream.Skip(8); @@ -330,11 +310,6 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.AnimationControl: - if (this.isSimplePng || this.animationControl is not null) - { - PngThrowHelper.ThrowInvalidAnimationControl(); - } - this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Physical: @@ -356,11 +331,6 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: - if (this.isSimplePng) - { - continue; - } - lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: @@ -379,11 +349,6 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Data: - if (this.animationControl is null) - { - this.isSimplePng = true; - } - // Spec says tRNS must be before IDAT so safe to exit. if (this.colorMetadataOnly) { @@ -465,7 +430,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat } EOF: - if (this.header is { Width: 0, Height: 0 }) + if (this.header.Width == 0 && this.header.Height == 0) { PngThrowHelper.ThrowInvalidHeader(); } @@ -568,7 +533,7 @@ private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan d /// The metadata information for the image /// The frame control information for the frame /// The image that we will populate - private void InitializeImage(ImageMetadata metadata, APngFrameControl? frameControl, out Image image) + private void InitializeImage(ImageMetadata metadata, FrameControl? frameControl, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( @@ -579,7 +544,7 @@ private void InitializeImage(ImageMetadata metadata, APngFrameControl? f if (frameControl is { } control) { - APngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAPngFrameMetadata(); + PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); frameMetadata.FromChunk(control); } @@ -603,12 +568,13 @@ private void InitializeImage(ImageMetadata metadata, APngFrameControl? f /// The type the pixels will be /// The frame control information for the frame /// The image that we will populate - private void InitializeFrame(APngFrameControl frameControl, Image image, out ImageFrame frame) + /// The created frame + private void InitializeFrame(FrameControl frameControl, Image image, out ImageFrame frame) where TPixel : unmanaged, IPixel { frame = image.Frames.CreateFrame(); - APngFrameMetadata frameMetadata = frame.Metadata.GetAPngFrameMetadata(); + PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata(); frameMetadata.FromChunk(frameControl); @@ -716,7 +682,7 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< { int currentRow = Adam7.FirstRow[0]; int currentRowBytesRead = 0; - int height = image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height; + int height = image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height; while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); @@ -763,7 +729,7 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); - ++currentRow; + currentRow++; } } @@ -784,7 +750,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I int pass = 0; int width = this.header.Width; int height = this.header.Height; - if (image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata)) + if (image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata)) { width = frameMetadata.Width; height = frameMetadata.Height; @@ -797,7 +763,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I if (numColumns == 0) { - ++pass; + pass++; // This pass contains no data; skip to next pass continue; @@ -1124,7 +1090,7 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM /// The containing data. private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data) { - this.animationControl = APngAnimationControl.Parse(data); + this.animationControl = AnimationControl.Parse(data); pngMetadata.NumberPlays = this.animationControl.NumberPlays; } @@ -1133,9 +1099,9 @@ private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan /// The containing data. - private APngFrameControl ReadFrameControlChunk(ReadOnlySpan data) + private FrameControl ReadFrameControlChunk(ReadOnlySpan data) { - APngFrameControl fcTL = APngFrameControl.Parse(data); + FrameControl fcTL = FrameControl.Parse(data); fcTL.Validate(this.header); diff --git a/src/ImageSharp/Formats/Png/APngDisposeOperation.cs b/src/ImageSharp/Formats/Png/PngDisposeOperation.cs similarity index 95% rename from src/ImageSharp/Formats/Png/APngDisposeOperation.cs rename to src/ImageSharp/Formats/Png/PngDisposeOperation.cs index 7b39a220d3..17a5091252 100644 --- a/src/ImageSharp/Formats/Png/APngDisposeOperation.cs +++ b/src/ImageSharp/Formats/Png/PngDisposeOperation.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). /// -public enum APngDisposeOperation +public enum PngDisposeOperation { /// /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index bf8b23b8f0..a4ae1ca0b9 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -19,11 +19,6 @@ public PngEncoder() => // quantizer with options appropriate to the encoding bit depth. this.Quantizer = null!; - /// - /// Gets whether the file is a simple PNG. - /// - public bool? IsSimplePng { get; init; } - /// /// Gets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1550417890..da2dc62103 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -25,7 +25,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The maximum block size, defaults at 64k for uncompressed blocks. /// - private const int MaxBlockSize = (1 << 16) - 1; + private const int MaxBlockSize = 65535; /// /// Used the manage memory allocations. @@ -168,20 +168,19 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - if ((this.encoder.IsSimplePng is null && targetImage.Frames.Count > 1) - || this.encoder.IsSimplePng is false) + if (targetImage.Frames.Count > 1) { this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); - this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetAPngFrameMetadata(), 0); + this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetPngFrameMetadata(), 0); _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false); int index = 1; foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1)) { - this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetAPngFrameMetadata(), index); - ++index; + this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetPngFrameMetadata(), index); + index++; IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame); index += this.WriteDataChunks(imageFrame, quantized, stream, true, index); quantized?.Dispose(); @@ -225,10 +224,10 @@ private static void ClearTransparentPixels(Image image) // TODO: We should be able to speed this up with SIMD and masking. Rgba32 rgba32 = default; Rgba32 transparent = Color.Transparent; - for (int y = 0; y < accessor.Height; ++y) + for (int y = 0; y < accessor.Height; y++) { Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; ++x) + for (int x = 0; x < accessor.Width; x++) { span[x].ToRgba32(ref rgba32); @@ -278,7 +277,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); // Can't map directly to byte array as it's big-endian. - for (int x = 0, o = 0; x < luminanceSpan.Length; ++x, o += 2) + for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2) { L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); @@ -318,7 +317,7 @@ private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan); // Can't map directly to byte array as it's big endian. - for (int x = 0, o = 0; x < laSpan.Length; ++x, o += 4) + for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4) { La32 la = Unsafe.Add(ref laRef, (uint)x); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L); @@ -602,11 +601,11 @@ private void WriteHeaderChunk(Stream stream) /// The number of times to loop this APNG. private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount) { - APngAnimationControl acTL = new(framesCount, playsCount); + AnimationControl acTL = new(framesCount, playsCount); acTL.WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, APngAnimationControl.Size); + this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size); } /// @@ -643,7 +642,7 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame? ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); // Loop, assign, and extract alpha values from the palette. - for (int i = 0; i < paletteLength; ++i) + for (int i = 0; i < paletteLength; i++) { Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, (uint)i); byte alpha = rgba.A; @@ -963,13 +962,13 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) /// The containing image data. /// Provides APng specific metadata information for the image frame. /// Sequence number. - private void WriteFrameControlChunk(Stream stream, APngFrameMetadata frameMetadata, int sequenceNumber) + private void WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, int sequenceNumber) { - APngFrameControl fcTL = APngFrameControl.FromMetadata(frameMetadata, sequenceNumber); + FrameControl fcTL = FrameControl.FromMetadata(frameMetadata, sequenceNumber); fcTL.WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, APngFrameControl.Size); + this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size); } /// @@ -1024,10 +1023,10 @@ private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame if (bufferLength % maxBlockSize != 0) { - ++numChunks; + numChunks++; } - for (int i = 0; i < numChunks; ++i) + for (int i = 0; i < numChunks; i++) { int length = bufferLength - (i * maxBlockSize); @@ -1078,7 +1077,7 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrame(ImageFrame pixels, IndexedImageFrame filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int y = 0; y < height; ++y) + for (int y = 0; y < height; y++) { this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); deflateStream.Write(filter); @@ -1201,7 +1200,7 @@ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantize col += Adam7.ColumnIncrement[pass]) { block[i] = srcRow[col]; - ++i; + i++; } // Encode data diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 292f087f27..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() { @@ -33,5 +33,5 @@ private PngFormat() public PngMetadata CreateDefaultFormatMetadata() => new(); /// - public APngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Png/APngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs similarity index 79% rename from src/ImageSharp/Formats/Png/APngFrameMetadata.cs rename to src/ImageSharp/Formats/Png/PngFrameMetadata.cs index f4f5fec916..76d4330562 100644 --- a/src/ImageSharp/Formats/Png/APngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -8,20 +8,20 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Provides APng specific metadata information for the image frame. /// -public class APngFrameMetadata : IDeepCloneable +public class PngFrameMetadata : IDeepCloneable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public APngFrameMetadata() + public PngFrameMetadata() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private APngFrameMetadata(APngFrameMetadata other) + private PngFrameMetadata(PngFrameMetadata other) { this.Width = other.Width; this.Height = other.Height; @@ -66,18 +66,18 @@ private APngFrameMetadata(APngFrameMetadata other) /// /// Gets or sets the type of frame area disposal to be done after rendering this frame /// - public APngDisposeOperation DisposeOperation { get; set; } + public PngDisposeOperation DisposeOperation { get; set; } /// /// Gets or sets the type of frame area rendering for this frame /// - public APngBlendOperation BlendOperation { get; set; } + public PngBlendOperation BlendOperation { get; set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The chunk to create an instance from. - internal void FromChunk(APngFrameControl frameControl) + internal void FromChunk(FrameControl frameControl) { this.Width = frameControl.Width; this.Height = frameControl.Height; @@ -90,5 +90,5 @@ internal void FromChunk(APngFrameControl frameControl) } /// - public IDeepCloneable DeepClone() => new APngFrameMetadata(this); + public IDeepCloneable DeepClone() => new PngFrameMetadata(this); } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 57608a9090..75d4b173c8 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,7 +13,6 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release - preview From 6e548223f2c2e16dc6c8301cef09e1bb1f3feb3c Mon Sep 17 00:00:00 2001 From: Poker Date: Thu, 17 Aug 2023 23:53:07 +0800 Subject: [PATCH 05/20] Fix review --- .../Formats/Png/Chunks/AnimationControl.cs | 18 +- .../Formats/Png/Chunks/FrameControl.cs | 12 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 37 ++- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Formats/Png/PngScanlineProcessor.cs | 212 +++++------------- src/ImageSharp/Formats/Png/PngThrowHelper.cs | 9 + 6 files changed, 115 insertions(+), 175 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs index 7a76e5a095..a9f99a9e4a 100644 --- a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs @@ -5,21 +5,25 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; -internal record AnimationControl( - int NumberFrames, - int NumberPlays) +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; } = NumberFrames; + public int NumberFrames { get; } /// /// Gets the number of times to loop this APNG. 0 indicates infinite looping. /// - public int NumberPlays { get; } = NumberPlays; + public int NumberPlays { get; } /// /// Writes the acTL to the given buffer. @@ -38,6 +42,6 @@ public void WriteTo(Span buffer) /// The parsed acTL. public static AnimationControl Parse(ReadOnlySpan data) => new( - NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]), - NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8])); + 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 index 5f0bc716dc..0414840a85 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -86,32 +86,32 @@ public void Validate(PngHeader hdr) { if (this.XOffset < 0) { - throw new NotSupportedException($"Invalid XOffset. Expected >= 0. Was '{this.XOffset}'."); + PngThrowHelper.ThrowInvalidParameter(this.XOffset, "Expected >= 0"); } if (this.YOffset < 0) { - throw new NotSupportedException($"Invalid YOffset. Expected >= 0. Was '{this.YOffset}'."); + PngThrowHelper.ThrowInvalidParameter(this.YOffset, "Expected >= 0"); } if (this.Width <= 0) { - throw new NotSupportedException($"Invalid Width. Expected > 0. Was '{this.Width}'."); + PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0"); } if (this.Height <= 0) { - throw new NotSupportedException($"Invalid Height. Expected > 0. Was '{this.Height}'."); + PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0"); } if (this.XOffset + this.Width > hdr.Width) { - throw new NotSupportedException($"Invalid XOffset or Width. The sum > PngHeader.Width. Was '{this.XOffset + this.Width}'."); + PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); } if (this.YOffset + this.Height > hdr.Height) { - throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'."); + PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, "The sum > PngHeader.Height"); } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index ac9aa5fad0..618ca42dfa 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -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; @@ -61,7 +66,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The png animation control. /// - private AnimationControl? animationControl; + private AnimationControl animationControl; /// /// The number of bytes per pixel. @@ -116,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; } @@ -124,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; @@ -139,6 +146,7 @@ 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; @@ -174,10 +182,21 @@ public Image Decode(BufferedReadStream stream, CancellationToken ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: + ++frameCount; + if (frameCount == this.maxFrames) + { + break; + } + currentFrame = null; lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: + if (frameCount == this.maxFrames) + { + break; + } + if (image is null) { PngThrowHelper.ThrowMissingDefaultData(); @@ -290,6 +309,7 @@ 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; @@ -331,9 +351,20 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; 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; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index da2dc62103..cc654b2e75 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1077,7 +1077,7 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrame( bool hasTrans, L16 luminance16Trans, L8 luminanceTrans) - 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 (!hasTrans) - { - 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) - { - 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(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromLa32(source); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); - 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(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; - - pixel.FromLa16(source); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedGrayscaleScanline( + header, + scanlineSpan, + rowSpan, + 0, + 1, + hasTrans, + luminance16Trans, + luminanceTrans); public static void ProcessInterlacedGrayscaleScanline( in PngHeader header, @@ -161,39 +110,15 @@ public static void ProcessGrayscaleWithAlphaScanline( 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( + header, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedGrayscaleWithAlphaScanline( in PngHeader header, @@ -244,48 +169,14 @@ public static void ProcessPaletteScanline( Span rowSpan, ReadOnlySpan palette, byte[] paletteAlpha) - where TPixel : unmanaged, IPixel - { - if (palette.IsEmpty) - { - PngThrowHelper.ThrowMissingPalette(); - } - - TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); - ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); - - if (paletteAlpha?.Length > 0) - { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - - for (nuint x = 0; x < (uint)header.Width; x++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, x); - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = 0; x < (uint)header.Width; x++) - { - int index = Unsafe.Add(ref scanlineSpanRef, x); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedPaletteScanline(header, + scanlineSpan, + rowSpan, + 0, + 1, + palette, + paletteAlpha); public static void ProcessInterlacedPaletteScanline( in PngHeader header, @@ -297,6 +188,11 @@ public static void ProcessInterlacedPaletteScanline( byte[] paletteAlpha) where TPixel : unmanaged, IPixel { + if (palette.IsEmpty) + { + PngThrowHelper.ThrowMissingPalette(); + } + TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -347,9 +243,9 @@ public static void ProcessRgbScanline( TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (!hasTrans) + if (header.BitDepth == 16) { - if (header.BitDepth == 16) + if (!hasTrans) { Rgb48 rgb48 = default; int o = 0; @@ -365,31 +261,27 @@ public static void ProcessRgbScanline( } else { - PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); + 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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; + + pixel.FromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } } return; } - if (header.BitDepth == 16) - { - 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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else + if (hasTrans) { Rgba32 rgba32 = default; ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); @@ -404,6 +296,10 @@ public static void ProcessRgbScanline( Unsafe.Add(ref rowSpanRef, x) = pixel; } } + else + { + PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); + } } public static void ProcessInterlacedRgbScanline( diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 78c243eeef..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; @@ -38,6 +39,14 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except [DoesNotReturn] public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); + [DoesNotReturn] + 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."); From c253f39a323ccd463f766a12598cd467550c6dd7 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 18 Aug 2023 01:38:30 +0800 Subject: [PATCH 06/20] Fix offset --- .../Formats/Png/Chunks/FrameControl.cs | 18 ++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 73 +++++----- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 71 +++++----- .../Formats/Png/PngScanlineProcessor.cs | 130 +++++++++++------- 4 files changed, 174 insertions(+), 118 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index 0414840a85..bb75cbabf8 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -56,6 +56,16 @@ public FrameControl( /// public int YOffset { get; } + /// + /// Gets the X limit at which to render the following frame + /// + public uint XLimit => (uint)(this.XOffset + this.Width); + + /// + /// Gets the Y limit at which to render the following frame + /// + public uint YLimit => (uint)(this.YOffset + this.Height); + /// /// Gets the frame delay fraction numerator /// @@ -104,14 +114,14 @@ public void Validate(PngHeader hdr) PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0"); } - if (this.XOffset + this.Width > hdr.Width) + if (this.XLimit > hdr.Width) { - PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); + PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); } - if (this.YOffset + this.Height > hdr.Height) + if (this.YLimit > hdr.Height) { - PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, "The sum > PngHeader.Height"); + PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 618ca42dfa..a53844b4a7 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -228,6 +228,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream.Position += 4; // Skip sequence number return length - 4; }, + lastFrameControl.Value, cancellationToken); lastFrameControl = null; break; @@ -237,7 +238,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.InitializeImage(metadata, lastFrameControl, out image); } - this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, cancellationToken); + FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default); + + this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, frameControl, cancellationToken); lastFrameControl = null; break; case PngChunkType.Palette: @@ -682,8 +685,9 @@ private int CalculateScanlineLength(int width) /// 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(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, CancellationToken cancellationToken) + private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, FrameControl frameControl, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using ZlibInflateStream deframeStream = new(this.currentStream, getData); @@ -692,11 +696,11 @@ private void ReadScanlines(int chunkLength, ImageFrame image, Pn 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); } } @@ -704,16 +708,17 @@ private void ReadScanlines(int chunkLength, ImageFrame image, Pn /// 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 png metadata /// The CancellationToken - private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodePixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = Adam7.FirstRow[0]; + int currentRow = frameControl.YOffset; int currentRowBytesRead = 0; - int height = image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height; + int height = frameControl.Height; while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); @@ -757,7 +762,7 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< break; } - this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); currentRow++; @@ -769,23 +774,19 @@ private void DecodePixelData(DeflateStream compressedStream, ImageFrame< /// /// /// The pixel format. + /// The frame control /// The compressed pixel data stream. /// The current image. /// The png metadata. /// The cancellation token. - private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodeInterlacedPixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = Adam7.FirstRow[0]; + int currentRow = Adam7.FirstRow[0] + frameControl.YOffset; int currentRowBytesRead = 0; int pass = 0; - int width = this.header.Width; - int height = this.header.Height; - if (image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata)) - { - width = frameMetadata.Width; - height = frameMetadata.Height; - } + int width = frameControl.Width; + int height = frameControl.Height; Buffer2D imageBuffer = image.PixelBuffer; while (true) @@ -848,7 +849,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I } Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); - this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); + this.ProcessInterlacedDefilteredScanline(frameControl, this.scanline.GetSpan(), rowSpan, pngMetadata, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); @@ -874,11 +875,12 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I /// Processes the de-filtered scanline filling the image pixel data /// /// The pixel format. + /// The frame control /// The index of the current scanline being processed. /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline(FrameControl frameControl, int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -902,7 +904,8 @@ private void ProcessDefilteredScanline(int currentRow, ReadOnlySpan(int currentRow, ReadOnlySpan(int currentRow, ReadOnlySpan(int currentRow, ReadOnlySpan(int currentRow, ReadOnlySpan(int currentRow, ReadOnlySpan /// The pixel format. + /// The frame control /// The de-filtered scanline /// The current image row. /// The png metadata. /// 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(FrameControl frameControl, ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel { // Trim the first marker byte from the buffer @@ -994,7 +999,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi { case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1007,7 +1013,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1019,7 +1026,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - this.header, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1031,7 +1038,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1046,7 +1054,8 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index cc654b2e75..1e7426226a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -172,23 +172,24 @@ public void Encode(Image image, Stream stream, CancellationToken { this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); - this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetPngFrameMetadata(), 0); - _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false); + FrameControl frameControl = this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetPngFrameMetadata(), 0); + _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); int index = 1; foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1)) { - this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetPngFrameMetadata(), index); + frameControl = this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetPngFrameMetadata(), index); index++; IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame); - index += this.WriteDataChunks(imageFrame, quantized, stream, true, index); + index += this.WriteDataChunks(frameControl, imageFrame, quantized, stream, true); quantized?.Dispose(); } } else { - _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false); + FrameControl frameControl = new(0, this.width, this.height, 0, 0, 0, 0, default, default); + _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); rootQuantized?.Dispose(); } @@ -962,25 +963,27 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) /// The containing image data. /// Provides APng specific metadata information for the image frame. /// Sequence number. - private void WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, int sequenceNumber) + private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, int sequenceNumber) { FrameControl fcTL = FrameControl.FromMetadata(frameMetadata, sequenceNumber); 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 frame control /// The frame. /// The quantized pixel data. Can be null. /// The stream. /// Is writing fdAT or IDAT. - /// Start sequence number. - private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame, int startSequenceNumber = 0) + private int WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -994,16 +997,16 @@ private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame { if (quantized is not null) { - this.EncodeAdam7IndexedPixels(quantized, deflateStream); + this.EncodeAdam7IndexedPixels(frameControl, quantized, deflateStream); } else { - this.EncodeAdam7Pixels(pixels, deflateStream); + this.EncodeAdam7Pixels(frameControl, pixels, deflateStream); } } else { - this.EncodePixels(pixels, quantized, deflateStream); + this.EncodePixels(frameControl, pixels, quantized, deflateStream); } } @@ -1038,7 +1041,7 @@ private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame if (isFrame) { byte[] chunkBuffer = new byte[MaxBlockSize]; - BinaryPrimitives.WriteInt32BigEndian(chunkBuffer, startSequenceNumber + i); + BinaryPrimitives.WriteInt32BigEndian(chunkBuffer, frameControl.SequenceNumber + 1 + i); buffer.AsSpan().Slice(i * maxBlockSize, length).CopyTo(chunkBuffer.AsSpan(4, length)); this.WriteChunk(stream, PngChunkType.FrameData, chunkBuffer, 0, length + 4); @@ -1069,19 +1072,15 @@ 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(ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = this.width; - int height = this.height; - if (pixels.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? pngMetadata)) - { - width = pngMetadata.Width; - height = pngMetadata.Height; - } + int width = frameControl.Width; + int height = frameControl.Height; int bytesPerScanline = this.CalculateScanlineLength(width); int filterLength = bytesPerScanline + 1; @@ -1094,7 +1093,7 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrame filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int y = 0; y < height; y++) + for (int y = frameControl.YOffset; y < frameControl.YLimit; y++) { this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); deflateStream.Write(filter); @@ -1107,18 +1106,19 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrame /// The type of the pixel. + /// The frame control /// The image frame. /// The deflate stream. - private void EncodeAdam7Pixels(ImageFrame frame, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frame.Width; - int height = frame.Height; + int width = frameControl.Width; + int height = frameControl.Height; 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] + frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1136,11 +1136,11 @@ private void EncodeAdam7Pixels(ImageFrame frame, ZlibDeflateStre Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; row < height; row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < frameControl.YLimit; row += Adam7.RowIncrement[pass]) { // 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.XLimit; col += Adam7.ColumnIncrement[pass]) { block[i++] = srcRow[col]; } @@ -1160,17 +1160,18 @@ private void EncodeAdam7Pixels(ImageFrame frame, ZlibDeflateStre /// 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 = frameControl.Width; + int height = frameControl.Height; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass]; - int startCol = Adam7.FirstColumn[pass]; + int startRow = Adam7.FirstRow[pass] + frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1190,13 +1191,13 @@ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantize Span attempt = attemptBuffer.GetSpan(); for (int row = startRow; - row < height; + row < frameControl.YLimit; row += Adam7.RowIncrement[pass]) { // Collect data ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); for (int col = startCol, i = 0; - col < width; + col < frameControl.XLimit; col += Adam7.ColumnIncrement[pass]) { block[i] = srcRow[col]; diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 125aa75b75..67a1f7c82d 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -16,7 +16,8 @@ namespace SixLabors.ImageSharp.Formats.Png; internal static class PngScanlineProcessor { public static void ProcessGrayscaleScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, bool hasTrans, @@ -24,7 +25,8 @@ public static void ProcessGrayscaleScanline( L8 luminanceTrans) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( - header, + bitDepth, + frameControl, scanlineSpan, rowSpan, 0, @@ -34,7 +36,8 @@ public static void ProcessGrayscaleScanline( luminanceTrans); public static void ProcessInterlacedGrayscaleScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -44,17 +47,18 @@ public static void ProcessInterlacedGrayscaleScanline( L8 luminanceTrans) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + (uint)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 (!hasTrans) { - 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.XLimit; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); pixel.FromL16(Unsafe.As(ref luminance)); @@ -63,7 +67,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.XLimit; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); pixel.FromL8(Unsafe.As(ref luminance)); @@ -74,11 +78,11 @@ public static void ProcessInterlacedGrayscaleScanline( return; } - if (header.BitDepth == 16) + if (bitDepth == 16) { La32 source = default; int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2) + for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = luminance; @@ -92,7 +96,7 @@ public static void ProcessInterlacedGrayscaleScanline( { La16 source = default; byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); source.L = luminance; @@ -105,14 +109,16 @@ public static void ProcessInterlacedGrayscaleScanline( } public static void ProcessGrayscaleWithAlphaScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, uint bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleWithAlphaScanline( - header, + bitDepth, + frameControl, scanlineSpan, rowSpan, 0, @@ -121,7 +127,8 @@ public static void ProcessGrayscaleWithAlphaScanline( bytesPerSample); public static void ProcessInterlacedGrayscaleWithAlphaScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -130,15 +137,16 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( uint bytesPerSample) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + (uint)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.XLimit; x += increment, o += 4) { source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); @@ -150,27 +158,28 @@ 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.XLimit; 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, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, ReadOnlySpan palette, byte[] paletteAlpha) where TPixel : unmanaged, IPixel => - ProcessInterlacedPaletteScanline(header, + ProcessInterlacedPaletteScanline( + frameControl, scanlineSpan, rowSpan, 0, @@ -179,7 +188,7 @@ public static void ProcessPaletteScanline( paletteAlpha); public static void ProcessInterlacedPaletteScanline( - in PngHeader header, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -193,6 +202,7 @@ public static void ProcessInterlacedPaletteScanline( PngThrowHelper.ThrowMissingPalette(); } + uint offset = pixelOffset + (uint)frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -205,7 +215,7 @@ public static void ProcessInterlacedPaletteScanline( // channel and we should try to read it. Rgba32 rgba = default; ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) { uint index = Unsafe.Add(ref scanlineSpanRef, o); rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; @@ -217,7 +227,7 @@ public static void ProcessInterlacedPaletteScanline( } else { - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) { int index = Unsafe.Add(ref scanlineSpanRef, o); Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); @@ -229,8 +239,8 @@ public static void ProcessInterlacedPaletteScanline( } public static void ProcessRgbScanline( - Configuration configuration, - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, @@ -240,16 +250,17 @@ public static void ProcessRgbScanline( Rgb24 rgb24Trans) where TPixel : unmanaged, IPixel { + uint offset = (uint)frameControl.XOffset; TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (header.BitDepth == 16) + if (bitDepth == 16) { if (!hasTrans) { Rgb48 rgb48 = default; int o = 0; - for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x++, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -264,7 +275,7 @@ public static void ProcessRgbScanline( Rgb48 rgb48 = default; Rgba64 rgba64 = default; int o = 0; - for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x++, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -286,7 +297,7 @@ public static void ProcessRgbScanline( Rgba32 rgba32 = default; ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); - for (nuint x = 0; x < (uint)header.Width; x++) + for (nuint x = offset; x < frameControl.XLimit; x++) { ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x); rgba32.Rgb = rgb24; @@ -298,12 +309,23 @@ public static void ProcessRgbScanline( } else { - PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); + ReadOnlySpan source = MemoryMarshal.Cast(scanlineSpan)[..frameControl.Width]; + ref Rgb24 sourceBaseRef = ref MemoryMarshal.GetReference(source); + ref TPixel destBaseRef = ref MemoryMarshal.GetReference(rowSpan); + + for (nuint i = offset; i < frameControl.XLimit; i++) + { + ref Rgb24 sp = ref Unsafe.Add(ref sourceBaseRef, i); + ref TPixel dp = ref Unsafe.Add(ref destBaseRef, i); + + dp.FromRgb24(sp); + } } } public static void ProcessInterlacedRgbScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -315,18 +337,19 @@ public static void ProcessInterlacedRgbScanline( Rgb24 rgb24Trans) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + (uint)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) { if (hasTrans) { 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.XLimit; x += increment, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -343,7 +366,7 @@ public static void ProcessInterlacedRgbScanline( { Rgb48 rgb48 = default; int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -361,7 +384,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.XLimit; x += increment, o += bytesPerPixel) { rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); @@ -376,7 +399,7 @@ public static void ProcessInterlacedRgbScanline( { Rgb24 rgb = default; int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) { rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); @@ -389,22 +412,23 @@ public static void ProcessInterlacedRgbScanline( } public static void ProcessRgbaScanline( - Configuration configuration, - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample) where TPixel : unmanaged, IPixel { + uint offset = (uint)frameControl.XOffset; TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (header.BitDepth == 16) + if (bitDepth == 16) { Rgba64 rgba64 = default; int o = 0; - for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x++, o += bytesPerPixel) { rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -417,12 +441,23 @@ public static void ProcessRgbaScanline( } else { - PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width); + ReadOnlySpan source = MemoryMarshal.Cast(scanlineSpan)[..frameControl.Width]; + ref Rgba32 sourceBaseRef = ref MemoryMarshal.GetReference(source); + ref TPixel destBaseRef = ref MemoryMarshal.GetReference(rowSpan); + + for (nuint i = offset; i < frameControl.XLimit; i++) + { + ref Rgba32 sp = ref Unsafe.Add(ref sourceBaseRef, i); + ref TPixel dp = ref Unsafe.Add(ref destBaseRef, i); + + dp.FromRgba32(sp); + } } } public static void ProcessInterlacedRgbaScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -431,15 +466,16 @@ public static void ProcessInterlacedRgbaScanline( int bytesPerSample) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + (uint)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.XLimit; x += increment, o += bytesPerPixel) { rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -454,7 +490,7 @@ public static void ProcessInterlacedRgbaScanline( { Rgba32 rgba = default; int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) { rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); From 146406494acf96245932e2230e7e396b0a825253 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 18 Aug 2023 01:43:16 +0800 Subject: [PATCH 07/20] Fix: replace lambda with method --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 36 ++++++++++---------- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 1 - 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a53844b4a7..a426208944 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -213,23 +213,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } this.currentStream.Position += 4; - this.ReadScanlines( - chunk.Length - 4, - currentFrame, - pngMetadata, - () => - { - int length = this.ReadNextDataChunk(); - if (this.ReadNextDataChunk() is 0) - { - return length; - } - - this.currentStream.Position += 4; // Skip sequence number - return length - 4; - }, - lastFrameControl.Value, - cancellationToken); + this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, lastFrameControl.Value, cancellationToken); lastFrameControl = null; break; case PngChunkType.Data: @@ -1576,7 +1560,7 @@ 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)) { @@ -1592,6 +1576,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. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1e7426226a..17fb40446d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -240,7 +240,6 @@ private static void ClearTransparentPixels(Image image) } }); } - } /// From 316a8395b6b5e266d5b89b540592e09a6ec9e406 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 18 Aug 2023 10:07:46 +0800 Subject: [PATCH 08/20] Optimize code --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 8 +- .../Formats/Png/PngScanlineProcessor.cs | 155 ++++-------------- 2 files changed, 38 insertions(+), 125 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a426208944..dce94b5cec 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -671,7 +671,7 @@ private int CalculateScanlineLength(int width) /// A delegate to get more data from the inner stream for . /// The frame control /// The cancellation token. - private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, FrameControl frameControl, 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, getData); @@ -763,7 +763,7 @@ private void DecodePixelData(FrameControl frameControl, DeflateStream co /// The current image. /// The png metadata. /// The cancellation token. - private void DecodeInterlacedPixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodeInterlacedPixelData(in FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int currentRow = Adam7.FirstRow[0] + frameControl.YOffset; @@ -864,7 +864,7 @@ private void DecodeInterlacedPixelData(FrameControl frameControl, Deflat /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(FrameControl frameControl, int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline(in FrameControl frameControl, int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -961,7 +961,7 @@ private void ProcessDefilteredScanline(FrameControl frameControl, int cu /// The png metadata. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. - private void ProcessInterlacedDefilteredScanline(FrameControl frameControl, ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) + private void ProcessInterlacedDefilteredScanline(in FrameControl frameControl, ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel { // Trim the first marker byte from the buffer diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 67a1f7c82d..85fc2b120e 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -17,7 +17,7 @@ internal static class PngScanlineProcessor { public static void ProcessGrayscaleScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, bool hasTrans, @@ -37,7 +37,7 @@ public static void ProcessGrayscaleScanline( public static void ProcessInterlacedGrayscaleScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -110,7 +110,7 @@ public static void ProcessInterlacedGrayscaleScanline( public static void ProcessGrayscaleWithAlphaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, @@ -128,7 +128,7 @@ public static void ProcessGrayscaleWithAlphaScanline( public static void ProcessInterlacedGrayscaleWithAlphaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -172,7 +172,7 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( } public static void ProcessPaletteScanline( - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, ReadOnlySpan palette, @@ -188,7 +188,7 @@ public static void ProcessPaletteScanline( paletteAlpha); public static void ProcessInterlacedPaletteScanline( - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -240,7 +240,7 @@ public static void ProcessInterlacedPaletteScanline( public static void ProcessRgbScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, @@ -248,84 +248,23 @@ public static void ProcessRgbScanline( bool hasTrans, Rgb48 rgb48Trans, Rgb24 rgb24Trans) - where TPixel : unmanaged, IPixel - { - uint offset = (uint)frameControl.XOffset; - TPixel pixel = default; - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - - if (bitDepth == 16) - { - if (!hasTrans) - { - Rgb48 rgb48 = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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 - { - Rgb48 rgb48 = default; - Rgba64 rgba64 = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - - return; - } - - if (hasTrans) - { - Rgba32 rgba32 = default; - ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); - ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); - for (nuint x = offset; x < frameControl.XLimit; x++) - { - ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x); - rgba32.Rgb = rgb24; - rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue; - - pixel.FromRgba32(rgba32); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - ReadOnlySpan source = MemoryMarshal.Cast(scanlineSpan)[..frameControl.Width]; - ref Rgb24 sourceBaseRef = ref MemoryMarshal.GetReference(source); - ref TPixel destBaseRef = ref MemoryMarshal.GetReference(rowSpan); - - for (nuint i = offset; i < frameControl.XLimit; i++) - { - ref Rgb24 sp = ref Unsafe.Add(ref sourceBaseRef, i); - ref TPixel dp = ref Unsafe.Add(ref destBaseRef, i); - - dp.FromRgb24(sp); - } - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample, + hasTrans, + rgb48Trans, + rgb24Trans); public static void ProcessInterlacedRgbScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -413,51 +352,25 @@ public static void ProcessInterlacedRgbScanline( public static void ProcessRgbaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample) - where TPixel : unmanaged, IPixel - { - uint offset = (uint)frameControl.XOffset; - TPixel pixel = default; - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - - if (bitDepth == 16) - { - Rgba64 rgba64 = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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 - { - ReadOnlySpan source = MemoryMarshal.Cast(scanlineSpan)[..frameControl.Width]; - ref Rgba32 sourceBaseRef = ref MemoryMarshal.GetReference(source); - ref TPixel destBaseRef = ref MemoryMarshal.GetReference(rowSpan); - - for (nuint i = offset; i < frameControl.XLimit; i++) - { - ref Rgba32 sp = ref Unsafe.Add(ref sourceBaseRef, i); - ref TPixel dp = ref Unsafe.Add(ref destBaseRef, i); - - dp.FromRgba32(sp); - } - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbaScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedRgbaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -468,7 +381,6 @@ public static void ProcessInterlacedRgbaScanline( { uint offset = pixelOffset + (uint)frameControl.XOffset; TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); if (bitDepth == 16) @@ -488,6 +400,7 @@ public static void ProcessInterlacedRgbaScanline( } else { + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); Rgba32 rgba = default; int o = 0; for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) From a6b8abe3e9cf08d3701ae29d9ea7a8b2fb85e22e Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 22 Aug 2023 08:00:49 +0800 Subject: [PATCH 09/20] remove set to null from disposal --- src/ImageSharp/Formats/Png/PngEncoder.cs | 9 ++++----- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index a4ae1ca0b9..a8c1de939f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -1,5 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#nullable disable using SixLabors.ImageSharp.Advanced; @@ -13,11 +14,9 @@ public class PngEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - public PngEncoder() => - - // We set the quantizer to null here to allow the underlying encoder to create a - // quantizer with options appropriate to the encoding bit depth. - this.Quantizer = null!; + // We set the quantizer to null here to allow the underlying encoder to create a + // quantizer with options appropriate to the encoding bit depth. + public PngEncoder() => this.Quantizer = null; /// /// Gets the number of bits per sample or per palette index (not per pixel). diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 17fb40446d..509837e706 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -206,8 +206,6 @@ public void Dispose() { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); - this.previousScanline = null!; - this.currentScanline = null!; } /// From aada974cdb883a84fe69eb52e98a496c06bdd0cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 22:13:05 +1000 Subject: [PATCH 10/20] Refactor and cleanup --- .../Formats/Png/Chunks/FrameControl.cs | 119 ++++----- ...PngBlendOperation.cs => PngBlendMethod.cs} | 4 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 57 ++-- ...sposeOperation.cs => PngDisposalMethod.cs} | 4 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 248 +++++++++++------- .../Formats/Png/PngFrameMetadata.cs | 48 +--- .../Formats/Png/PngScanlineProcessor.cs | 176 +++++-------- 7 files changed, 309 insertions(+), 347 deletions(-) rename src/ImageSharp/Formats/Png/{PngBlendOperation.cs => PngBlendMethod.cs} (93%) rename src/ImageSharp/Formats/Png/{PngDisposeOperation.cs => PngDisposalMethod.cs} (92%) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index bb75cbabf8..c7233ada14 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -10,22 +10,22 @@ internal readonly struct FrameControl public const int Size = 26; public FrameControl( - int sequenceNumber, - int width, - int height, - int xOffset, - int yOffset, - short delayNumber, - short delayDenominator, - PngDisposeOperation disposeOperation, - PngBlendOperation blendOperation) + 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.DelayNumber = delayNumber; + this.DelayNumerator = delayNumerator; this.DelayDenominator = delayDenominator; this.DisposeOperation = disposeOperation; this.BlendOperation = blendOperation; @@ -34,130 +34,101 @@ public FrameControl( /// /// Gets the sequence number of the animation chunk, starting from 0 /// - public int SequenceNumber { get; } + public uint SequenceNumber { get; } /// /// Gets the width of the following frame /// - public int Width { get; } + public uint Width { get; } /// /// Gets the height of the following frame /// - public int Height { get; } + public uint Height { get; } /// /// Gets the X position at which to render the following frame /// - public int XOffset { get; } + public uint XOffset { get; } /// /// Gets the Y position at which to render the following frame /// - public int YOffset { get; } + public uint YOffset { get; } /// /// Gets the X limit at which to render the following frame /// - public uint XLimit => (uint)(this.XOffset + this.Width); + public uint XMax => this.XOffset + this.Width; /// /// Gets the Y limit at which to render the following frame /// - public uint YLimit => (uint)(this.YOffset + this.Height); + public uint YMax => this.YOffset + this.Height; /// /// Gets the frame delay fraction numerator /// - public short DelayNumber { get; } + public ushort DelayNumerator { get; } /// /// Gets the frame delay fraction denominator /// - public short DelayDenominator { get; } + public ushort DelayDenominator { get; } /// /// Gets the type of frame area disposal to be done after rendering this frame /// - public PngDisposeOperation DisposeOperation { get; } + public PngDisposalMethod DisposeOperation { get; } /// /// Gets the type of frame area rendering for this frame /// - public PngBlendOperation BlendOperation { get; } + public PngBlendMethod BlendOperation { get; } /// /// Validates the APng fcTL. /// + /// The header. /// /// Thrown if the image does pass validation. /// - public void Validate(PngHeader hdr) + public void Validate(PngHeader header) { - if (this.XOffset < 0) - { - PngThrowHelper.ThrowInvalidParameter(this.XOffset, "Expected >= 0"); - } - - if (this.YOffset < 0) - { - PngThrowHelper.ThrowInvalidParameter(this.YOffset, "Expected >= 0"); - } - - if (this.Width <= 0) + if (this.Width == 0) { PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0"); } - if (this.Height <= 0) + if (this.Height == 0) { PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0"); } - if (this.XLimit > hdr.Width) + if (this.XMax > header.Width) { - PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); + PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); } - if (this.YLimit > hdr.Height) + if (this.YMax > header.Height) { - PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); + PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); } } - /// - /// Parses the APngFrameControl from the given metadata. - /// - /// The metadata to parse. - /// Sequence number. - public static FrameControl FromMetadata(PngFrameMetadata frameMetadata, int sequenceNumber) - { - FrameControl fcTL = new( - sequenceNumber, - frameMetadata.Width, - frameMetadata.Height, - frameMetadata.XOffset, - frameMetadata.YOffset, - frameMetadata.DelayNumber, - frameMetadata.DelayDenominator, - frameMetadata.DisposeOperation, - frameMetadata.BlendOperation); - return fcTL; - } - /// /// Writes the fcTL to the given buffer. /// /// The buffer to write to. public void WriteTo(Span buffer) { - BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber); - BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width); - BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); - BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); - BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); - BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber); - BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator); + 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; @@ -170,13 +141,13 @@ public void WriteTo(Span buffer) /// The parsed fcTL. public static FrameControl Parse(ReadOnlySpan data) => new( - sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]), - width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]), - height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]), - xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]), - yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]), - delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]), - delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]), - disposeOperation: (PngDisposeOperation)data[24], - blendOperation: (PngBlendOperation)data[25]); + 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/PngBlendOperation.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs similarity index 93% rename from src/ImageSharp/Formats/Png/PngBlendOperation.cs rename to src/ImageSharp/Formats/Png/PngBlendMethod.cs index b8a84a933e..b7ace9ccfd 100644 --- a/src/ImageSharp/Formats/Png/PngBlendOperation.cs +++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Png; @@ -6,7 +6,7 @@ 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 PngBlendOperation +public enum PngBlendMethod { /// /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 23942dd98d..deb01289ea 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -152,7 +152,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream = stream; this.currentStream.Skip(8); Image? image = null; - FrameControl? lastFrameControl = null; + FrameControl? previousFrameControl = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -182,14 +182,14 @@ public Image Decode(BufferedReadStream stream, CancellationToken ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: - ++frameCount; + frameCount++; if (frameCount == this.maxFrames) { break; } currentFrame = null; - lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + previousFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: if (frameCount == this.maxFrames) @@ -202,33 +202,32 @@ public Image Decode(BufferedReadStream stream, CancellationToken PngThrowHelper.ThrowMissingDefaultData(); } - if (lastFrameControl is null) + if (previousFrameControl is null) { PngThrowHelper.ThrowMissingFrameControl(); } if (currentFrame is null) { - this.InitializeFrame(lastFrameControl.Value, image, out currentFrame); + this.InitializeFrame(previousFrameControl.Value, image, out currentFrame); } this.currentStream.Position += 4; - this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, lastFrameControl.Value, cancellationToken); - lastFrameControl = null; + this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, previousFrameControl.Value, cancellationToken); + previousFrameControl = null; break; case PngChunkType.Data: if (image is null) { - this.InitializeImage(metadata, lastFrameControl, out image); + this.InitializeImage(metadata, previousFrameControl, out image); // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default); - - this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, frameControl, cancellationToken); - lastFrameControl = null; + FrameControl frameControl = previousFrameControl ?? new(0, (uint)this.header.Width, (uint)this.header.Height, 0, 0, 0, 0, default, default); + this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, in frameControl, cancellationToken); + previousFrameControl = null; break; case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); @@ -705,9 +704,9 @@ private void ReadScanlines(int chunkLength, ImageFrame image, Pn private void DecodePixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = frameControl.YOffset; + int currentRow = (int)frameControl.YOffset; int currentRowBytesRead = 0; - int height = frameControl.Height; + int height = (int)frameControl.YMax; while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); @@ -771,11 +770,11 @@ private void DecodePixelData(FrameControl frameControl, DeflateStream co private void DecodeInterlacedPixelData(in FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = Adam7.FirstRow[0] + frameControl.YOffset; + int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; int currentRowBytesRead = 0; int pass = 0; - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; Buffer2D imageBuffer = image.PixelBuffer; while (true) @@ -792,7 +791,7 @@ private void DecodeInterlacedPixelData(in FrameControl frameControl, Def int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (currentRow < height) + while (currentRow < endRow) { cancellationToken.ThrowIfCancellationRequested(); while (currentRowBytesRead < bytesPerInterlaceScanline) @@ -894,7 +893,7 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.TransparentColor); @@ -904,7 +903,7 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)this.bytesPerPixel, @@ -914,7 +913,7 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.ColorTable); @@ -923,6 +922,7 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( + this.configuration, this.header.BitDepth, frameControl, scanlineSpan, @@ -935,8 +935,9 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -984,7 +985,7 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -996,7 +997,7 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1008,7 +1009,7 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1019,8 +1020,9 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1033,8 +1035,9 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, diff --git a/src/ImageSharp/Formats/Png/PngDisposeOperation.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs similarity index 92% rename from src/ImageSharp/Formats/Png/PngDisposeOperation.cs rename to src/ImageSharp/Formats/Png/PngDisposalMethod.cs index 17a5091252..17391de95c 100644 --- a/src/ImageSharp/Formats/Png/PngDisposeOperation.cs +++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Png; @@ -6,7 +6,7 @@ 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 PngDisposeOperation +public enum PngDisposalMethod { /// /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 0eabeeb857..6c86d1b106 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -111,6 +111,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// private const string ColorProfileName = "ICC Profile"; + /// + /// The encoder quantizer, if present. + /// + private IQuantizer? quantizer; + /// /// Initializes a new instance of the class. /// @@ -121,6 +126,7 @@ public PngEncoderCore(Configuration configuration, PngEncoder encoder) this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; + this.quantizer = encoder.Quantizer; } /// @@ -140,63 +146,81 @@ 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; - Image targetImage = image; + + stream.Write(PngConstants.HeaderBytes); + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WriteColorProfileChunk(stream, metadata); + + ImageFrame? clonedFrame = null; + ImageFrame currentFrame = image.Frames.RootFrame; + bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; if (clearTransparency) { - targetImage = clonedImage = image.Clone(); - ClearTransparentPixels(targetImage); + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame); } - IndexedImageFrame? rootQuantized = this.CreateQuantizedImageAndUpdateBitDepth(targetImage.Frames.RootFrame); + IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null); + this.WritePaletteChunk(stream, quantized); - stream.Write(PngConstants.HeaderBytes); - - this.WriteHeaderChunk(stream); - this.WriteGammaChunk(stream); - this.WriteColorProfileChunk(stream, metadata); - this.WritePaletteChunk(stream, rootQuantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - if (targetImage.Frames.Count > 1) + if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); + this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.NumberPlays); + + // 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. - FrameControl frameControl = this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetPngFrameMetadata(), 0); - _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); + // Write the first frame. + FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); - int index = 1; + // Capture the global palette for reuse on subsequent frames. + ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); - foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1)) + // Write following frames. + for (int i = 1; i < image.Frames.Count; i++) { - frameControl = this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetPngFrameMetadata(), index); - index++; - IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame); - index += this.WriteDataChunks(frameControl, imageFrame, quantized, stream, true); + currentFrame = image.Frames[i]; + if (clearTransparency) + { + // Dispose of previous clone and reassign. + clonedFrame?.Dispose(); + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame); + } + + frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i); + + // Dispose of previous quantized frame and reassign. quantized?.Dispose(); + quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); } } else { - FrameControl frameControl = new(0, this.width, this.height, 0, 0, 0, 0, default, default); - _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); - rootQuantized?.Dispose(); + FrameControl frameControl = new(0, (uint)this.width, (uint)this.height, 0, 0, 0, 0, default, default); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); } this.WriteEndChunk(stream); stream.Flush(); - clonedImage?.Dispose(); - rootQuantized?.Dispose(); + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); + quantized?.Dispose(); } /// @@ -210,46 +234,44 @@ public void Dispose() /// 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) + /// The cloned image frame where the transparent pixels will be changed. + private static void ClearTransparentPixels(ImageFrame clone) where TPixel : unmanaged, IPixel - { - foreach (ImageFrame imageFrame in image.Frames) + => clone.ProcessPixelRows(accessor => { - imageFrame.ProcessPixelRows(accessor => + // TODO: We should be able to speed this up with SIMD and masking. + Rgba32 rgba32 = default; + Rgba32 transparent = Color.Transparent; + for (int y = 0; y < accessor.Height; y++) { - // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 rgba32 = default; - Rgba32 transparent = Color.Transparent; - for (int y = 0; y < accessor.Height; y++) + Span span = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) { - Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) - { - span[x].ToRgba32(ref rgba32); + span[x].ToRgba32(ref rgba32); - if (rgba32.A is 0) - { - span[x].FromRgba32(transparent); - } + if (rgba32.A is 0) + { + span[x].FromRgba32(transparent); } } - }); - } - } + } + }); /// /// Creates the quantized image and calculates and sets the bit depth. /// /// The type of the pixel. + /// The image metadata. /// The frame to quantize. + /// Any previously derived palette. /// The quantized image. private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( - ImageFrame frame) + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { - IndexedImageFrame? quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, frame); - + IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -914,7 +936,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) } Span alpha = this.chunkDataBuffer.Span; - switch (pngMetadata.ColorType) + if (pngMetadata.ColorType == PngColorType.Rgb) { if (this.use16Bit) { @@ -957,11 +979,23 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) /// Writes the animation control chunk to the stream. /// /// The containing image data. - /// Provides APng specific metadata information for the image frame. - /// Sequence number. - private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, int sequenceNumber) + /// The image frame. + /// The frame sequence number. + private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber) { - FrameControl fcTL = FrameControl.FromMetadata(frameMetadata, 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: frameMetadata.DelayNumerator, + delayDenominator: frameMetadata.DelayDenominator, + disposeOperation: frameMetadata.DisposalMethod, + blendOperation: frameMetadata.BlendMethod); fcTL.WriteTo(this.chunkDataBuffer.Span); @@ -1036,11 +1070,8 @@ private int WriteDataChunks(FrameControl frameControl, ImageFrame(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int height = (int)frameControl.Height; int bytesPerScanline = this.CalculateScanlineLength(width); int filterLength = bytesPerScanline + 1; @@ -1089,7 +1120,7 @@ private void EncodePixels(FrameControl frameControl, ImageFrame { Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int y = frameControl.YOffset; y < frameControl.YLimit; 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); @@ -1108,13 +1139,13 @@ private void EncodePixels(FrameControl frameControl, ImageFrame private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + 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] + frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; + 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 @@ -1132,11 +1163,11 @@ private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; row < frameControl.YLimit; row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < height; row += Adam7.RowIncrement[pass]) { // Collect pixel data Span srcRow = pixelBuffer.DangerousGetRowSpan(row); - for (int col = startCol, i = 0; col < frameControl.XLimit; col += Adam7.ColumnIncrement[pass]) + for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) { block[i++] = srcRow[col]; } @@ -1162,12 +1193,12 @@ private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass] + frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; + 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 @@ -1186,14 +1217,12 @@ private void EncodeAdam7IndexedPixels(FrameControl frameControl, Indexed Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; - row < frameControl.YLimit; - 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 < frameControl.XLimit; + col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) { block[i] = srcRow[col]; @@ -1229,7 +1258,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) @@ -1255,6 +1284,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. /// @@ -1335,12 +1396,16 @@ private void SanitizeAndSetEncoderOptions( /// The png encoder. /// The color type. /// The bits per component. - /// The frame. - private static IndexedImageFrame? CreateQuantizedFrame( + /// The image metadata. + /// The frame to quantize. + /// Any previously derived palette. + private IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, PngColorType colorType, byte bitDepth, - ImageFrame frame) + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { if (colorType is not PngColorType.Palette) @@ -1348,25 +1413,30 @@ private void SanitizeAndSetEncoderOptions( 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) { - // TODO: Can APNG have per-frame color tables? - 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(frame.Configuration); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index 76d4330562..a68d45ae0c 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -23,55 +23,31 @@ public PngFrameMetadata() /// The metadata to create an instance from. private PngFrameMetadata(PngFrameMetadata other) { - this.Width = other.Width; - this.Height = other.Height; - this.XOffset = other.XOffset; - this.YOffset = other.YOffset; - this.DelayNumber = other.DelayNumber; + this.DelayNumerator = other.DelayNumerator; this.DelayDenominator = other.DelayDenominator; - this.DisposeOperation = other.DisposeOperation; - this.BlendOperation = other.BlendOperation; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; } - /// - /// Gets or sets the width of the following frame - /// - public int Width { get; set; } - - /// - /// Gets or sets the height of the following frame - /// - public int Height { get; set; } - - /// - /// Gets or sets the X position at which to render the following frame - /// - public int XOffset { get; set; } - - /// - /// Gets or sets the Y position at which to render the following frame - /// - public int YOffset { get; set; } - /// /// Gets or sets the frame delay fraction numerator /// - public short DelayNumber { get; set; } + public ushort DelayNumerator { get; set; } /// /// Gets or sets the frame delay fraction denominator /// - public short DelayDenominator { get; set; } + public ushort DelayDenominator { get; set; } /// /// Gets or sets the type of frame area disposal to be done after rendering this frame /// - public PngDisposeOperation DisposeOperation { get; set; } + public PngDisposalMethod DisposalMethod { get; set; } /// /// Gets or sets the type of frame area rendering for this frame /// - public PngBlendOperation BlendOperation { get; set; } + public PngBlendMethod BlendMethod { get; set; } /// /// Initializes a new instance of the class. @@ -79,14 +55,10 @@ private PngFrameMetadata(PngFrameMetadata other) /// The chunk to create an instance from. internal void FromChunk(FrameControl frameControl) { - this.Width = frameControl.Width; - this.Height = frameControl.Height; - this.XOffset = frameControl.XOffset; - this.YOffset = frameControl.YOffset; - this.DelayNumber = frameControl.DelayNumber; + this.DelayNumerator = frameControl.DelayNumerator; this.DelayDenominator = frameControl.DelayDenominator; - this.DisposeOperation = frameControl.DisposeOperation; - this.BlendOperation = frameControl.BlendOperation; + this.DisposalMethod = frameControl.DisposeOperation; + this.BlendMethod = frameControl.BlendOperation; } /// diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 9d219e1de5..31a59188e3 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -20,9 +20,7 @@ public static void ProcessGrayscaleScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( bitDepth, @@ -31,9 +29,7 @@ public static void ProcessGrayscaleScanline( rowSpan, 0, 1, - hasTrans, - luminance16Trans, - luminanceTrans); + transparentColor); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -56,7 +52,7 @@ public static void ProcessInterlacedGrayscaleScanline( if (bitDepth == 16) { int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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)); @@ -65,7 +61,7 @@ public static void ProcessInterlacedGrayscaleScanline( } else { - for (nuint x = offset, o = 0; x < frameControl.XLimit; 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)); @@ -81,7 +77,7 @@ public static void ProcessInterlacedGrayscaleScanline( L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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; @@ -95,8 +91,7 @@ public static void ProcessInterlacedGrayscaleScanline( { byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); - for (nuint x = offset, o = 0; x < frameControl.XLimit; 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; @@ -146,7 +141,7 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( { La32 source = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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)); @@ -159,7 +154,7 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( { La16 source = default; nuint offset2 = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment) + for (nuint x = offset; x < frameControl.XMax; x += increment) { source.L = Unsafe.Add(ref scanlineSpanRef, offset2); source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample); @@ -175,8 +170,7 @@ public static void ProcessPaletteScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -184,8 +178,7 @@ public static void ProcessPaletteScanline( rowSpan, 0, 1, - palette, - paletteAlpha); + palette); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -193,8 +186,7 @@ public static void ProcessInterlacedPaletteScanline( Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { if (palette is null) @@ -202,53 +194,31 @@ public static void ProcessInterlacedPaletteScanline( PngThrowHelper.ThrowMissingPalette(); } - uint offset = pixelOffset + (uint)frameControl.XOffset; 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++) + for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++) { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, o); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) - { - int index = Unsafe.Add(ref scanlineSpanRef, o); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, o); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } public static void ProcessRgbScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbScanline( + configuration, bitDepth, frameControl, scanlineSpan, @@ -257,11 +227,10 @@ public static void ProcessRgbScanline( 1, bytesPerPixel, bytesPerSample, - hasTrans, - rgb48Trans, - rgb24Trans); + transparentColor); public static void ProcessInterlacedRgbScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -274,36 +243,18 @@ public static void ProcessInterlacedRgbScanline( where TPixel : unmanaged, IPixel { uint offset = pixelOffset + (uint)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 (bitDepth == 16) + if (transparentColor is null) { - if (hasTrans) - { - Rgb48 rgb48 = default; - Rgba64 rgba64 = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, 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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else + if (bitDepth == 16) { Rgb48 rgb48 = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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)); @@ -315,30 +266,33 @@ public static void ProcessInterlacedRgbScanline( } else { - Rgb24 rgb = default; - int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) - { - rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + // Rgb24 rgb = default; + // int o = 0; + // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + // { + // rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + // rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + // rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + + // pixel.FromRgb24(rgb); + // Unsafe.Add(ref rowSpanRef, x) = pixel; + // } + + // PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); + PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); } 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)); @@ -357,7 +311,7 @@ public static void ProcessInterlacedRgbScanline( Rgba32 rgba = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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)); @@ -368,23 +322,10 @@ public static void ProcessInterlacedRgbScanline( Unsafe.Add(ref rowSpanRef, x) = pixel; } } - else - { - Rgb24 rgb = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - { - rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } } public static void ProcessRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -393,6 +334,7 @@ public static void ProcessRgbaScanline( int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( + configuration, bitDepth, frameControl, scanlineSpan, @@ -403,6 +345,7 @@ public static void ProcessRgbaScanline( bytesPerSample); public static void ProcessInterlacedRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -421,7 +364,7 @@ public static void ProcessInterlacedRgbaScanline( { Rgba64 rgba64 = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; 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)); @@ -434,19 +377,22 @@ public static void ProcessInterlacedRgbaScanline( } else { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - Rgba32 rgba = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - { - rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + // ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + // Rgba32 rgba = default; + // int o = 0; + // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + // { + // rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + // rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + // rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + // rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + + // pixel.FromRgba32(rgba); + // Unsafe.Add(ref rowSpanRef, x) = pixel; + // } + + // PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width); + PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); } } } From 564c3d122c8e5c82e01963f2ee58d640feafb958 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 22:55:07 +1000 Subject: [PATCH 11/20] Fix encoding --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 15 ++++++++++----- .../Formats/Png/PngDecoderTests.cs | 7 ++++++- .../Formats/Png/PngEncoderTests.cs | 6 ++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 6c86d1b106..bbf1a64534 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -189,6 +189,7 @@ public void Encode(Image image, Stream stream, CancellationToken ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); // Write following frames. + uint increment = 0; for (int i = 1; i < image.Frames.Count; i++) { currentFrame = image.Frames[i]; @@ -200,12 +201,14 @@ public void Encode(Image image, Stream stream, CancellationToken ClearTransparentPixels(currentFrame); } - frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i); + // 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); - this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); + increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); } } else @@ -1013,7 +1016,7 @@ private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame /// The quantized pixel data. Can be null. /// The stream. /// Is writing fdAT or IDAT. - private int WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame) + private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -1070,7 +1073,9 @@ private int WriteDataChunks(FrameControl frameControl, ImageFrame(FrameControl frameControl, ImageFrame diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 57d0619b99..9f11bf6507 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -111,7 +111,12 @@ public void Decode(TestImageProvider provider) public void Decode_APng(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(PngDecoder.Instance); // MagickReferenceDecoder cannot decode APNGs + using Image image = provider.GetImage(PngDecoder.Instance); + + Assert.Equal(5, image.Frames.Count); + + // TODO: Assertations. + // MagickReferenceDecoder cannot decode APNGs (Though ImageMagick can, we likely need to update our mapping implementation) } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 2c37dc4713..f6dfcd178f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -451,8 +451,14 @@ public void Encode_APng(TestImageProvider provider) 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); + + // TODO: Additional assertations regarding metadata. + Assert.Equal(5, image.Frames.Count); } [Theory] From 3bc12e43ca82818d12f132418dad22646f675626 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 23:29:28 +1000 Subject: [PATCH 12/20] Fix failing tests --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 2 - .../Formats/Png/PngScanlineProcessor.cs | 67 +++++++++---------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index deb01289ea..f84d936c81 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -935,7 +935,6 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( - this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, @@ -1035,7 +1034,6 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( - this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 31a59188e3..82faef3fe2 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -41,7 +41,7 @@ public static void ProcessInterlacedGrayscaleScanline( Color? transparentColor) where TPixel : unmanaged, IPixel { - uint offset = pixelOffset + (uint)frameControl.XOffset; + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -132,7 +132,7 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( uint bytesPerSample) where TPixel : unmanaged, IPixel { - uint offset = pixelOffset + (uint)frameControl.XOffset; + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -242,7 +242,7 @@ public static void ProcessInterlacedRgbScanline( Color? transparentColor) where TPixel : unmanaged, IPixel { - uint offset = pixelOffset + (uint)frameControl.XOffset; + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); @@ -266,20 +266,18 @@ public static void ProcessInterlacedRgbScanline( } else { - // Rgb24 rgb = default; - // int o = 0; - // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - // { - // rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - // rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - // rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - - // pixel.FromRgb24(rgb); - // Unsafe.Add(ref rowSpanRef, x) = pixel; - // } - - // PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); - PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); + // TODO: Investigate reintroducing bulk operations optimization here. + Rgb24 rgb = default; + int o = 0; + 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)); + rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + + pixel.FromRgb24(rgb); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } } return; @@ -325,7 +323,6 @@ public static void ProcessInterlacedRgbScanline( } public static void ProcessRgbaScanline( - Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -334,7 +331,6 @@ public static void ProcessRgbaScanline( int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( - configuration, bitDepth, frameControl, scanlineSpan, @@ -345,7 +341,6 @@ public static void ProcessRgbaScanline( bytesPerSample); public static void ProcessInterlacedRgbaScanline( - Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -356,7 +351,7 @@ public static void ProcessInterlacedRgbaScanline( int bytesPerSample) where TPixel : unmanaged, IPixel { - uint offset = pixelOffset + (uint)frameControl.XOffset; + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -377,22 +372,20 @@ public static void ProcessInterlacedRgbaScanline( } else { - // ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - // Rgba32 rgba = default; - // int o = 0; - // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - // { - // rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - // rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - // rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - // rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - - // pixel.FromRgba32(rgba); - // Unsafe.Add(ref rowSpanRef, x) = pixel; - // } - - // PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width); - PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); + // TODO: Investigate reintroducing bulk operations optimization here. + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + Rgba32 rgba = default; + int o = 0; + 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)); + rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + + pixel.FromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } } } } From 0385ad03157128659e40c9937300cd62325bc670 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Oct 2023 15:44:41 +1000 Subject: [PATCH 13/20] Fix header bit depth assignment. --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index bbf1a64534..8eabde8d9d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -150,9 +150,6 @@ public void Encode(Image image, Stream stream, CancellationToken this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); stream.Write(PngConstants.HeaderBytes); - this.WriteHeaderChunk(stream); - this.WriteGammaChunk(stream); - this.WriteColorProfileChunk(stream, metadata); ImageFrame? clonedFrame = null; ImageFrame currentFrame = image.Frames.RootFrame; @@ -164,9 +161,13 @@ public void Encode(Image image, Stream stream, CancellationToken ClearTransparentPixels(currentFrame); } + // Do not move this. We require an accurate bit depth for the header chunk. IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null); - this.WritePaletteChunk(stream, quantized); + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WriteColorProfileChunk(stream, metadata); + this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); From 5ed6f24943b697d2c354d762ebaaa6aecee27274 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 21:54:53 +1000 Subject: [PATCH 14/20] Reintroduce scanline optimizations --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 20 +++++- .../Formats/Png/PngFrameMetadata.cs | 2 +- .../Formats/Png/PngScanlineProcessor.cs | 65 ++++++++++++------- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f84d936c81..8484fd0c6d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -567,7 +567,7 @@ private void InitializeImage(ImageMetadata metadata, FrameControl? frame if (frameControl is { } control) { PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); - frameMetadata.FromChunk(control); + frameMetadata.FromChunk(in control); } this.bytesPerPixel = this.CalculateBytesPerPixel(); @@ -837,7 +837,13 @@ private void DecodeInterlacedPixelData(in FrameControl frameControl, Def } Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); - this.ProcessInterlacedDefilteredScanline(frameControl, this.scanline.GetSpan(), rowSpan, pngMetadata, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); + this.ProcessInterlacedDefilteredScanline( + frameControl, + this.scanline.GetSpan(), + rowSpan, + pngMetadata, + pixelOffset: Adam7.FirstColumn[pass], + increment: Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); @@ -935,6 +941,7 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, @@ -961,7 +968,13 @@ private void ProcessDefilteredScanline(in FrameControl frameControl, int /// The png metadata. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. - private void ProcessInterlacedDefilteredScanline(in FrameControl frameControl, ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) + private void ProcessInterlacedDefilteredScanline( + in FrameControl frameControl, + ReadOnlySpan defilteredScanline, + Span rowSpan, + PngMetadata pngMetadata, + int pixelOffset = 0, + int increment = 1) where TPixel : unmanaged, IPixel { // Trim the first marker byte from the buffer @@ -1034,6 +1047,7 @@ private void ProcessInterlacedDefilteredScanline(in FrameControl frameCo case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( + this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index a68d45ae0c..3325c6ba1a 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -53,7 +53,7 @@ private PngFrameMetadata(PngFrameMetadata other) /// Initializes a new instance of the class. /// /// The chunk to create an instance from. - internal void FromChunk(FrameControl frameControl) + internal void FromChunk(in FrameControl frameControl) { this.DelayNumerator = frameControl.DelayNumerator; this.DelayDenominator = frameControl.DelayDenominator; diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 82faef3fe2..f217515e3c 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -216,18 +216,18 @@ public static void ProcessRgbScanline( int bytesPerPixel, int bytesPerSample, Color? transparentColor) - where TPixel : unmanaged, IPixel => - ProcessInterlacedRgbScanline( - configuration, - bitDepth, - frameControl, - scanlineSpan, - rowSpan, - 0, - 1, - bytesPerPixel, - bytesPerSample, - transparentColor); + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample, + transparentColor); public static void ProcessInterlacedRgbScanline( Configuration configuration, @@ -264,9 +264,16 @@ 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 { - // TODO: Investigate reintroducing bulk operations optimization here. Rgb24 rgb = default; int o = 0; for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) @@ -323,24 +330,27 @@ public static void ProcessInterlacedRgbScanline( } public static void ProcessRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample) - where TPixel : unmanaged, IPixel => - ProcessInterlacedRgbaScanline( - bitDepth, - frameControl, - scanlineSpan, - rowSpan, - 0, - 1, - bytesPerPixel, - bytesPerSample); + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbaScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -370,9 +380,16 @@ 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 { - // TODO: Investigate reintroducing bulk operations optimization here. ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); Rgba32 rgba = default; int o = 0; From bc5b6c519b92badc7079efcfcffa545275a94682 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 22:29:58 +1000 Subject: [PATCH 15/20] Add alpha blending support --- .../Formats/Png/Chunks/FrameControl.cs | 5 ++ src/ImageSharp/Formats/Png/PngBlendMethod.cs | 8 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 73 ++++++++++++++++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index c7233ada14..b912e9b09a 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -9,6 +9,11 @@ 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, diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs index b7ace9ccfd..f71dce8325 100644 --- a/src/ImageSharp/Formats/Png/PngBlendMethod.cs +++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors. +// 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. +/// 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 { @@ -14,7 +15,8 @@ public enum PngBlendMethod Source, /// - /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. Note that the second variation of the sample code is applicable. + /// 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/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8484fd0c6d..776e52a331 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -153,6 +153,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream.Skip(8); Image? image = null; FrameControl? previousFrameControl = null; + ImageFrame? previousFrame = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -213,7 +214,21 @@ public Image Decode(BufferedReadStream stream, CancellationToken } this.currentStream.Position += 4; - this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, previousFrameControl.Value, cancellationToken); + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + this.ReadNextDataChunkAndSkipSeq, + previousFrameControl.Value, + cancellationToken); + + PngFrameMetadata pngFrameMetadata = currentFrame.Metadata.GetPngFrameMetadata(); + if (previousFrame != null && pngFrameMetadata.BlendMethod == PngBlendMethod.Over) + { + this.AlphaBlend(previousFrame, currentFrame); + } + + previousFrame = currentFrame; previousFrameControl = null; break; case PngChunkType.Data: @@ -225,8 +240,15 @@ public Image Decode(BufferedReadStream stream, CancellationToken AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - FrameControl frameControl = previousFrameControl ?? new(0, (uint)this.header.Width, (uint)this.header.Height, 0, 0, 0, 0, default, default); - this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, in frameControl, cancellationToken); + FrameControl frameControl = previousFrameControl ?? new((uint)this.header.Width, (uint)this.header.Height); + this.ReadScanlines( + chunk.Length, + image.Frames.RootFrame, + pngMetadata, + this.ReadNextDataChunk, + in frameControl, + cancellationToken); + previousFrameControl = null; break; case PngChunkType.Palette: @@ -698,10 +720,15 @@ private void ReadScanlines(int chunkLength, ImageFrame image, Pn /// 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(FrameControl frameControl, 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 { int currentRow = (int)frameControl.YOffset; @@ -750,8 +777,7 @@ private void DecodePixelData(FrameControl frameControl, DeflateStream co break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata); - + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata); this.SwapScanlineBuffers(); currentRow++; } @@ -759,7 +785,6 @@ private void DecodePixelData(FrameControl frameControl, DeflateStream co /// /// Decodes the raw interlaced pixel data row by row - /// /// /// The pixel format. /// The frame control @@ -767,7 +792,12 @@ private void DecodePixelData(FrameControl frameControl, DeflateStream co /// The current image. /// The png metadata. /// The cancellation token. - private void DecodeInterlacedPixelData(in FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodeInterlacedPixelData( + in FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame image, + PngMetadata pngMetadata, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; @@ -845,6 +875,7 @@ private void DecodeInterlacedPixelData(in FrameControl frameControl, Def pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); + // TODO: Alpha blending. this.SwapScanlineBuffers(); currentRow += Adam7.RowIncrement[pass]; @@ -874,7 +905,12 @@ private void DecodeInterlacedPixelData(in FrameControl frameControl, Def /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(in FrameControl frameControl, int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline( + in FrameControl frameControl, + int currentRow, + ReadOnlySpan defilteredScanline, + ImageFrame pixels, + PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -1841,4 +1877,21 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); + + private void AlphaBlend(ImageFrame src, ImageFrame dst) + where TPixel : unmanaged, IPixel + { + Buffer2D srcPixels = src.PixelBuffer; + Buffer2D dstPixels = dst.PixelBuffer; + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + + for (int y = 0; y < src.Height; y++) + { + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); + + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); + } + } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8eabde8d9d..ef179e8261 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -214,7 +214,7 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - FrameControl frameControl = new(0, (uint)this.width, (uint)this.height, 0, 0, 0, 0, default, default); + FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); } From 845527587c8bd8788ac79e401527ddf71e8b33dc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 22:52:53 +1000 Subject: [PATCH 16/20] Handle disposal methods. --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 25 ++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 776e52a331..317207da0f 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -210,7 +210,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken if (currentFrame is null) { - this.InitializeFrame(previousFrameControl.Value, image, out currentFrame); + this.InitializeFrame(previousFrameControl.Value, image, previousFrame, out currentFrame); } this.currentStream.Position += 4; @@ -612,14 +612,31 @@ private void InitializeImage(ImageMetadata metadata, FrameControl? frame /// The type the pixels will be /// The frame control information for the frame /// The image that we will populate + /// The previous frame. /// The created frame - private void InitializeFrame(FrameControl frameControl, Image image, out ImageFrame frame) + private void InitializeFrame( + FrameControl frameControl, + Image image, + ImageFrame? previousFrame, + out ImageFrame frame) where TPixel : unmanaged, IPixel { - frame = image.Frames.CreateFrame(); + // 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); - PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata(); + // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + if (frameControl.DisposeOperation == PngDisposalMethod.Background + || (previousFrame is null && frameControl.DisposeOperation == PngDisposalMethod.Previous)) + { + Rectangle restoreArea = new((int)frameControl.XOffset, (int)frameControl.YOffset, (int)frameControl.Width, (int)frameControl.Height); + Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea); + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); + pixelRegion.Clear(); + } + PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata(); frameMetadata.FromChunk(frameControl); this.previousScanline?.Dispose(); From 56588d3a1a612b64062f676015a12cc21106a47f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Oct 2023 09:23:08 +1000 Subject: [PATCH 17/20] Use region for alpha blending --- src/ImageSharp/Formats/Png/Chunks/FrameControl.cs | 2 ++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index b912e9b09a..fb2ca473c2 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -91,6 +91,8 @@ public FrameControl( /// public PngBlendMethod BlendOperation { get; } + public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height); + /// /// Validates the APng fcTL. /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 317207da0f..8c7c7c30d7 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -225,7 +225,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken PngFrameMetadata pngFrameMetadata = currentFrame.Metadata.GetPngFrameMetadata(); if (previousFrame != null && pngFrameMetadata.BlendMethod == PngBlendMethod.Over) { - this.AlphaBlend(previousFrame, currentFrame); + this.AlphaBlend(previousFrame, currentFrame, previousFrameControl.Value.Bounds); } previousFrame = currentFrame; @@ -630,7 +630,7 @@ private void InitializeFrame( if (frameControl.DisposeOperation == PngDisposalMethod.Background || (previousFrame is null && frameControl.DisposeOperation == PngDisposalMethod.Previous)) { - Rectangle restoreArea = new((int)frameControl.XOffset, (int)frameControl.YOffset, (int)frameControl.Width, (int)frameControl.Height); + Rectangle restoreArea = frameControl.Bounds; Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea); Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); pixelRegion.Clear(); @@ -1895,15 +1895,15 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); - private void AlphaBlend(ImageFrame src, ImageFrame dst) + private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D srcPixels = src.PixelBuffer; - Buffer2D dstPixels = dst.PixelBuffer; + Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); + Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); - for (int y = 0; y < src.Height; y++) + for (int y = 0; y < srcPixels.Height; y++) { Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); From 66f444d200985720a9ff3e27a7a52d4722836b43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 30 Oct 2023 23:52:41 +1000 Subject: [PATCH 18/20] Fix alpha blending and add tests --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 174 +++++++++++------- .../ImageFrameCollection{TPixel}.cs | 1 - .../Formats/Png/PngDecoderTests.cs | 26 ++- tests/ImageSharp.Tests/TestImages.cs | 12 +- .../TestUtilities/ImagingTestCaseUtility.cs | 28 +-- .../TestUtilities/TestImageExtensions.cs | 7 +- .../Tests/TestImageProviderTests.cs | 8 +- .../00.png | 3 + .../01.png | 3 + .../00.png | 3 + .../01.png | 3 + .../00.png | 3 + .../01.png | 3 + .../02.png | 3 + .../00.png | 3 + .../01.png | 3 + .../02.png | 3 + .../03.png | 3 + .../04.png | 3 + .../05.png | 3 + .../06.png | 3 + .../07.png | 3 + .../08.png | 3 + .../104.png | 3 + .../112.png | 3 + .../120.png | 3 + .../128.png | 3 + .../16.png | 3 + .../24.png | 3 + .../32.png | 3 + .../40.png | 3 + .../48.png | 3 + .../56.png | 3 + .../64.png | 3 + .../72.png | 3 + .../80.png | 3 + .../88.png | 3 + .../96.png | 3 + .../00.png | 3 + .../00.png | 3 + .../01.png | 3 + .../02.png | 3 + .../00.png | 3 + .../01.png | 3 + .../02.png | 3 + .../00.png | 3 + .../01.png | 3 + .../02.png | 3 + .../03.png | 3 + .../04.png | 3 + .../Png/animated/12-dispose-prev-first.png | 3 + .../14-dispose-background-before-region.png | 3 + .../animated/15-dispose-background-region.png | 3 + .../Png/animated/21-blend-over-multiple.png | 3 + .../Png/animated/4-split-idat-zero-length.png | 3 + .../Input/Png/animated/7-dispose-none.png | 3 + .../Png/animated/8-dispose-background.png | 3 + .../Images/Input/Png/{ => animated}/apng.png | 0 58 files changed, 302 insertions(+), 104 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png create mode 100644 tests/Images/Input/Png/animated/12-dispose-prev-first.png create mode 100644 tests/Images/Input/Png/animated/14-dispose-background-before-region.png create mode 100644 tests/Images/Input/Png/animated/15-dispose-background-region.png create mode 100644 tests/Images/Input/Png/animated/21-blend-over-multiple.png create mode 100644 tests/Images/Input/Png/animated/4-split-idat-zero-length.png create mode 100644 tests/Images/Input/Png/animated/7-dispose-none.png create mode 100644 tests/Images/Input/Png/animated/8-dispose-background.png rename tests/Images/Input/Png/{ => animated}/apng.png (100%) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8c7c7c30d7..08f9865490 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -153,6 +153,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream.Skip(8); Image? image = null; FrameControl? previousFrameControl = null; + FrameControl? currentFrameControl = null; ImageFrame? previousFrame = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -190,7 +191,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } currentFrame = null; - previousFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: if (frameCount == this.maxFrames) @@ -203,15 +204,13 @@ public Image Decode(BufferedReadStream stream, CancellationToken PngThrowHelper.ThrowMissingDefaultData(); } - if (previousFrameControl is null) + if (currentFrameControl is null) { PngThrowHelper.ThrowMissingFrameControl(); } - if (currentFrame is null) - { - this.InitializeFrame(previousFrameControl.Value, image, previousFrame, out currentFrame); - } + 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( @@ -219,37 +218,33 @@ public Image Decode(BufferedReadStream stream, CancellationToken currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, - previousFrameControl.Value, + currentFrameControl.Value, cancellationToken); - PngFrameMetadata pngFrameMetadata = currentFrame.Metadata.GetPngFrameMetadata(); - if (previousFrame != null && pngFrameMetadata.BlendMethod == PngBlendMethod.Over) - { - this.AlphaBlend(previousFrame, currentFrame, previousFrameControl.Value.Bounds); - } - previousFrame = currentFrame; - previousFrameControl = null; + previousFrameControl = currentFrameControl; break; case PngChunkType.Data: + + currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); if (image is null) { - this.InitializeImage(metadata, previousFrameControl, 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); } - FrameControl frameControl = previousFrameControl ?? new((uint)this.header.Width, (uint)this.header.Height); this.ReadScanlines( chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, - in frameControl, + currentFrameControl.Value, cancellationToken); - previousFrameControl = null; + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; break; case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); @@ -577,7 +572,7 @@ private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan d /// The metadata information for the image /// The frame control information for the frame /// The image that we will populate - private void InitializeImage(ImageMetadata metadata, FrameControl? frameControl, out Image image) + private void InitializeImage(ImageMetadata metadata, FrameControl frameControl, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( @@ -586,11 +581,8 @@ private void InitializeImage(ImageMetadata metadata, FrameControl? frame this.header.Height, metadata); - if (frameControl is { } control) - { - PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); - frameMetadata.FromChunk(in control); - } + PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); + frameMetadata.FromChunk(in frameControl); this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; @@ -610,12 +602,14 @@ private void InitializeImage(ImageMetadata metadata, FrameControl? frame /// Initializes the image and various buffers needed for processing /// /// The type the pixels will be - /// The frame control information for the frame + /// 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 frameControl, + FrameControl previousFrameControl, + FrameControl currentFrameControl, Image image, ImageFrame? previousFrame, out ImageFrame frame) @@ -627,17 +621,17 @@ private void InitializeFrame( 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 (frameControl.DisposeOperation == PngDisposalMethod.Background - || (previousFrame is null && frameControl.DisposeOperation == PngDisposalMethod.Previous)) + if (previousFrameControl.DisposeOperation == PngDisposalMethod.Background + || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.Previous)) { - Rectangle restoreArea = frameControl.Bounds; + 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(frameControl); + frameMetadata.FromChunk(currentFrameControl); this.previousScanline?.Dispose(); this.scanline?.Dispose(); @@ -714,12 +708,18 @@ private int CalculateScanlineLength(int width) /// A delegate to get more data from the inner stream for . /// The frame control /// The cancellation token. - private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, in FrameControl frameControl, 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, getData); - deframeStream.AllocateNewBytes(chunkLength, true); - DeflateStream dataStream = deframeStream.CompressedStream!; + using ZlibInflateStream inflateStream = new(this.currentStream, getData); + inflateStream.AllocateNewBytes(chunkLength, true); + DeflateStream dataStream = inflateStream.CompressedStream!; if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { @@ -751,13 +751,23 @@ private void DecodePixelData( 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 (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, currentRowBytesRead, this.bytesPerScanline - currentRowBytesRead); + int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); if (bytesRead <= 0) { return; @@ -794,10 +804,12 @@ private void DecodePixelData( break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata); + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata, blendRowBuffer); this.SwapScanlineBuffers(); currentRow++; } + + blendMemory?.Dispose(); } /// @@ -806,13 +818,13 @@ private void DecodePixelData( /// 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( in FrameControl frameControl, DeflateStream compressedStream, - ImageFrame image, + ImageFrame imageFrame, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -823,7 +835,16 @@ private void DecodeInterlacedPixelData( int width = (int)frameControl.Width; int endRow = (int)frameControl.YMax; - Buffer2D imageBuffer = image.PixelBuffer; + 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); @@ -889,10 +910,11 @@ private void DecodeInterlacedPixelData( this.scanline.GetSpan(), rowSpan, pngMetadata, + blendRowBuffer, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); - // TODO: Alpha blending. + blendRowBuffer.Clear(); this.SwapScanlineBuffers(); currentRow += Adam7.RowIncrement[pass]; @@ -911,6 +933,8 @@ private void DecodeInterlacedPixelData( break; } } + + blendMemory?.Dispose(); } /// @@ -919,26 +943,34 @@ private void DecodeInterlacedPixelData( /// The pixel format. /// The frame control /// The index of the current scanline being processed. - /// The de-filtered scanline + /// The de-filtered scanline /// The image /// The png metadata. + /// A span used to temporarily hold the decoded row pixel data for alpha blending. private void ProcessDefilteredScanline( in FrameControl frameControl, int currentRow, - ReadOnlySpan defilteredScanline, + ReadOnlySpan scanline, ImageFrame pixels, - PngMetadata pngMetadata) + PngMetadata pngMetadata, + Span blendRowBuffer) where TPixel : unmanaged, IPixel { - Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(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; try { + // TODO: The allocation here could be per frame, not per scanline. ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( trimmed, this.bytesPerScanline - 1, @@ -1004,6 +1036,13 @@ private void ProcessDefilteredScanline( break; } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1f); + } } finally { @@ -1016,22 +1055,29 @@ private void ProcessDefilteredScanline( /// /// The pixel format. /// The frame control - /// The de-filtered scanline - /// The current image row. + /// 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( in FrameControl frameControl, - ReadOnlySpan defilteredScanline, - Span rowSpan, + 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; @@ -1112,6 +1158,13 @@ private void ProcessInterlacedDefilteredScanline( break; } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1f); + } } finally { @@ -1894,21 +1947,4 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); - - private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) - where TPixel : unmanaged, IPixel - { - Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); - Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); - PixelBlender blender = - PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); - - for (int y = 0; y < srcPixels.Height; y++) - { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); - } - } } 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/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 9f11bf6507..9345681149 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -78,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,16 +119,16 @@ public void Decode(TestImageProvider provider) } [Theory] - [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] - public void Decode_APng(TestImageProvider provider) - where TPixel : unmanaged, IPixel + [WithFileCollection(nameof(MultiFrameTestFiles), PixelTypes.Rgba32)] + public void Decode_VerifyAllFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(PngDecoder.Instance); - Assert.Equal(5, image.Frames.Count); - - // TODO: Assertations. - // MagickReferenceDecoder cannot decode APNGs (Though ImageMagick can, we likely need to update our mapping implementation) + // 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] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 4b7badfdc2..048b19dc5b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -61,7 +61,17 @@ public static class Png public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string XmpColorPalette = "Png/xmp-colorpalette.png"; - public const string APng = "Png/apng.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"; 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 6417d6691c..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); 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/apng.png b/tests/Images/Input/Png/animated/apng.png similarity index 100% rename from tests/Images/Input/Png/apng.png rename to tests/Images/Input/Png/animated/apng.png From 14a95a8ed666cda50d655f4c2d37ad17f29ade68 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 31 Oct 2023 12:26:03 +1000 Subject: [PATCH 19/20] Rename properties and add metadata tests --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 6 ++-- .../Formats/Png/PngFrameMetadata.cs | 18 ++++------ src/ImageSharp/Formats/Png/PngMetadata.cs | 6 ++-- src/ImageSharp/Primitives/Rational.cs | 17 ++++----- .../Formats/Png/PngEncoderTests.cs | 17 ++++++++- .../Formats/Png/PngFrameMetadataTests.cs | 35 +++++++++++++++++++ .../Formats/Png/PngMetadataTests.cs | 13 ++++++- 8 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 08f9865490..d8305a3f57 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1258,7 +1258,7 @@ private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ef179e8261..04e3b1d840 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -176,7 +176,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.NumberPlays); + 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 @@ -996,8 +996,8 @@ private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame height: (uint)imageFrame.Height, xOffset: 0, yOffset: 0, - delayNumerator: frameMetadata.DelayNumerator, - delayDenominator: frameMetadata.DelayDenominator, + delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator, + delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator, disposeOperation: frameMetadata.DisposalMethod, blendOperation: frameMetadata.BlendMethod); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index 3325c6ba1a..ca4d8c1f45 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -23,21 +23,18 @@ public PngFrameMetadata() /// The metadata to create an instance from. private PngFrameMetadata(PngFrameMetadata other) { - this.DelayNumerator = other.DelayNumerator; - this.DelayDenominator = other.DelayDenominator; + this.FrameDelay = other.FrameDelay; this.DisposalMethod = other.DisposalMethod; this.BlendMethod = other.BlendMethod; } /// - /// Gets or sets the frame delay fraction numerator + /// 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 ushort DelayNumerator { get; set; } - - /// - /// Gets or sets the frame delay fraction denominator - /// - public ushort DelayDenominator { get; set; } + public Rational FrameDelay { get; set; } /// /// Gets or sets the type of frame area disposal to be done after rendering this frame @@ -55,8 +52,7 @@ private PngFrameMetadata(PngFrameMetadata other) /// The chunk to create an instance from. internal void FromChunk(in FrameControl frameControl) { - this.DelayNumerator = frameControl.DelayNumerator; - this.DelayDenominator = frameControl.DelayDenominator; + this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator); this.DisposalMethod = frameControl.DisposeOperation; this.BlendMethod = frameControl.BlendOperation; } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 92b8572bf6..b113dbfc17 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -29,7 +29,7 @@ private PngMetadata(PngMetadata other) this.Gamma = other.Gamma; this.InterlaceMethod = other.InterlaceMethod; this.TransparentColor = other.TransparentColor; - this.NumberPlays = other.NumberPlays; + this.RepeatCount = other.RepeatCount; if (other.ColorTable?.Length > 0) { @@ -80,9 +80,9 @@ 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. TODO: RepeatCount!! + /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. /// - public int NumberPlays { get; set; } + public int RepeatCount { get; set; } /// public IDeepCloneable DeepClone() => new PngMetadata(this); 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/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index f6dfcd178f..92c07a27a6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -457,8 +457,23 @@ public void Encode_APng(TestImageProvider provider) using Image output = Image.Load(memStream); ImageComparer.Exact.VerifySimilarity(output, image); - // TODO: Additional assertations regarding metadata. 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] 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 4492934f1a..b3c122a7a8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -31,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); @@ -47,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] From b4e98059b467c6ce9f2d1d2b15c1151c0b4c09df Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 31 Oct 2023 15:37:23 +1000 Subject: [PATCH 20/20] Update PngDecoderTests.cs --- .../Formats/Png/PngDecoderTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 9345681149..2e11093db6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -80,13 +80,13 @@ public partial class PngDecoderTests 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.APng, + TestImages.Png.SplitIDatZeroLength, + TestImages.Png.DisposeNone, + TestImages.Png.DisposeBackground, + TestImages.Png.DisposeBackgroundRegion, + TestImages.Png.DisposePreviousFirst, + TestImages.Png.DisposeBackgroundBeforeRegion, TestImages.Png.BlendOverMultiple };