From 45f6f5b1548ac08e7ef2d5e6dab71e06f3a272e0 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 12 Aug 2023 08:31:22 +0800 Subject: [PATCH 01/44] Implement APNG decoder --- .../Compression/Zlib/ZlibInflateStream.cs | 8 +- .../Formats/Png/APngBlendOperation.cs | 20 ++ .../Formats/Png/APngDisposeOperation.cs | 25 ++ .../Formats/Png/APngFrameMetadata.cs | 94 ++++++ .../Png/Chunks/APngAnimationControl.cs | 43 +++ .../Formats/Png/Chunks/APngFrameControl.cs | 152 ++++++++++ .../Formats/Png/{ => Chunks}/PngHeader.cs | 2 +- .../{PhysicalChunkData.cs => PngPhysical.cs} | 14 +- .../Formats/Png/{ => Chunks}/PngTextData.cs | 2 +- .../Formats/Png/MetadataExtensions.cs | 21 +- src/ImageSharp/Formats/Png/PngChunk.cs | 7 +- src/ImageSharp/Formats/Png/PngChunkType.cs | 57 +++- src/ImageSharp/Formats/Png/PngConstants.cs | 27 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 283 ++++++++++++++---- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +- src/ImageSharp/Formats/Png/PngFormat.cs | 5 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 8 + .../Formats/Png/PngScanlineProcessor.cs | 1 + src/ImageSharp/Formats/Png/PngThrowHelper.cs | 17 +- src/ImageSharp/ImageSharp.csproj | 1 + .../Formats/Png/PngMetadataTests.cs | 30 ++ .../Formats/Png/PngTextDataTests.cs | 3 +- 22 files changed, 707 insertions(+), 118 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/APngBlendOperation.cs create mode 100644 src/ImageSharp/Formats/Png/APngDisposeOperation.cs create mode 100644 src/ImageSharp/Formats/Png/APngFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs create mode 100644 src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs rename src/ImageSharp/Formats/Png/{ => Chunks}/PngHeader.cs (98%) rename src/ImageSharp/Formats/Png/Chunks/{PhysicalChunkData.cs => PngPhysical.cs} (89%) rename src/ImageSharp/Formats/Png/{ => Chunks}/PngTextData.cs (99%) diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index 06a7c3928c..c9f9904363 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream /// public override int Read(byte[] buffer, int offset, int count) { - if (this.currentDataRemaining == 0) + if (this.currentDataRemaining is 0) { // Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks. this.currentDataRemaining = this.getData(); - if (this.currentDataRemaining == 0) + if (this.currentDataRemaining is 0) { return 0; } @@ -142,11 +142,11 @@ internal sealed class ZlibInflateStream : Stream // Keep reading data until we've reached the end of the stream or filled the buffer. int bytesRead = 0; offset += totalBytesRead; - while (this.currentDataRemaining == 0 && totalBytesRead < count) + while (this.currentDataRemaining is 0 && totalBytesRead < count) { this.currentDataRemaining = this.getData(); - if (this.currentDataRemaining == 0) + if (this.currentDataRemaining is 0) { return totalBytesRead; } diff --git a/src/ImageSharp/Formats/Png/APngBlendOperation.cs b/src/ImageSharp/Formats/Png/APngBlendOperation.cs new file mode 100644 index 0000000000..0e8cdb4289 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngBlendOperation.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer. +/// +public enum APngBlendOperation +{ + /// + /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. + /// + Source, + + /// + /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. Note that the second variation of the sample code is applicable. + /// + Over +} diff --git a/src/ImageSharp/Formats/Png/APngDisposeOperation.cs b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs new file mode 100644 index 0000000000..7b39a220d3 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngDisposeOperation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). +/// +public enum APngDisposeOperation +{ + /// + /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. + /// + None, + + /// + /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + /// + Background, + + /// + /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. + /// + Previous +} diff --git a/src/ImageSharp/Formats/Png/APngFrameMetadata.cs b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs new file mode 100644 index 0000000000..f4f5fec916 --- /dev/null +++ b/src/ImageSharp/Formats/Png/APngFrameMetadata.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Png.Chunks; + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Provides APng specific metadata information for the image frame. +/// +public class APngFrameMetadata : IDeepCloneable +{ + /// + /// Initializes a new instance of the class. + /// + public APngFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private APngFrameMetadata(APngFrameMetadata other) + { + this.Width = other.Width; + this.Height = other.Height; + this.XOffset = other.XOffset; + this.YOffset = other.YOffset; + this.DelayNumber = other.DelayNumber; + this.DelayDenominator = other.DelayDenominator; + this.DisposeOperation = other.DisposeOperation; + this.BlendOperation = other.BlendOperation; + } + + /// + /// Gets or sets the width of the following frame + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of the following frame + /// + public int Height { get; set; } + + /// + /// Gets or sets the X position at which to render the following frame + /// + public int XOffset { get; set; } + + /// + /// Gets or sets the Y position at which to render the following frame + /// + public int YOffset { get; set; } + + /// + /// Gets or sets the frame delay fraction numerator + /// + public short DelayNumber { get; set; } + + /// + /// Gets or sets the frame delay fraction denominator + /// + public short DelayDenominator { get; set; } + + /// + /// Gets or sets the type of frame area disposal to be done after rendering this frame + /// + public APngDisposeOperation DisposeOperation { get; set; } + + /// + /// Gets or sets the type of frame area rendering for this frame + /// + public APngBlendOperation BlendOperation { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The chunk to create an instance from. + internal void FromChunk(APngFrameControl frameControl) + { + this.Width = frameControl.Width; + this.Height = frameControl.Height; + this.XOffset = frameControl.XOffset; + this.YOffset = frameControl.YOffset; + this.DelayNumber = frameControl.DelayNumber; + this.DelayDenominator = frameControl.DelayDenominator; + this.DisposeOperation = frameControl.DisposeOperation; + this.BlendOperation = frameControl.BlendOperation; + } + + /// + public IDeepCloneable DeepClone() => new APngFrameMetadata(this); +} diff --git a/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs new file mode 100644 index 0000000000..ca8268cd5d --- /dev/null +++ b/src/ImageSharp/Formats/Png/Chunks/APngAnimationControl.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks; + +internal record APngAnimationControl( + int NumberFrames, + int NumberPlays) +{ + public const int Size = 8; + + /// + /// Gets the number of frames + /// + public int NumberFrames { get; } = NumberFrames; + + /// + /// Gets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public int NumberPlays { get; } = NumberPlays; + + /// + /// Writes the acTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays); + } + + /// + /// Parses the APngAnimationControl from the given data buffer. + /// + /// The data to parse. + /// The parsed acTL. + public static APngAnimationControl Parse(ReadOnlySpan data) + => new( + NumberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + NumberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8])); +} diff --git a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs new file mode 100644 index 0000000000..e239bd8e2e --- /dev/null +++ b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks; + +internal readonly struct APngFrameControl +{ + public const int Size = 26; + + public APngFrameControl( + int sequenceNumber, + int width, + int height, + int xOffset, + int yOffset, + short delayNumber, + short delayDenominator, + APngDisposeOperation disposeOperation, + APngBlendOperation blendOperation) + { + this.SequenceNumber = sequenceNumber; + this.Width = width; + this.Height = height; + this.XOffset = xOffset; + this.YOffset = yOffset; + this.DelayNumber = delayNumber; + this.DelayDenominator = delayDenominator; + this.DisposeOperation = disposeOperation; + this.BlendOperation = blendOperation; + } + + /// + /// Gets the sequence number of the animation chunk, starting from 0 + /// + public int SequenceNumber { get; } + + /// + /// Gets the width of the following frame + /// + public int Width { get; } + + /// + /// Gets the height of the following frame + /// + public int Height { get; } + + /// + /// Gets the X position at which to render the following frame + /// + public int XOffset { get; } + + /// + /// Gets the Y position at which to render the following frame + /// + public int YOffset { get; } + + /// + /// Gets the frame delay fraction numerator + /// + public short DelayNumber { get; } + + /// + /// Gets the frame delay fraction denominator + /// + public short DelayDenominator { get; } + + /// + /// Gets the type of frame area disposal to be done after rendering this frame + /// + public APngDisposeOperation DisposeOperation { get; } + + /// + /// Gets the type of frame area rendering for this frame + /// + public APngBlendOperation BlendOperation { get; } + + /// + /// Validates the APng fcTL. + /// + /// + /// Thrown if the image does pass validation. + /// + public void Validate(PngHeader hdr) + { + if (this.XOffset < 0) + { + throw new NotSupportedException($"Invalid XOffset. Expected >= 0. Was '{this.XOffset}'."); + } + + if (this.YOffset < 0) + { + throw new NotSupportedException($"Invalid YOffset. Expected >= 0. Was '{this.YOffset}'."); + } + + if (this.Width <= 0) + { + throw new NotSupportedException($"Invalid Width. Expected > 0. Was '{this.Width}'."); + } + + if (this.Height <= 0) + { + throw new NotSupportedException($"Invalid Height. Expected > 0. Was '{this.Height}'."); + } + + if (this.XOffset + this.Width > hdr.Width) + { + throw new NotSupportedException($"Invalid XOffset or Width. The sum > PngHeader.Width. Was '{this.XOffset + this.Width}'."); + } + + if (this.YOffset + this.Height > hdr.Height) + { + throw new NotSupportedException($"Invalid YOffset or Height. The sum > PngHeader.Height. Was '{this.YOffset + this.Height}'."); + } + } + + /// + /// Writes the fcTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width); + BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); + BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); + BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); + BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber); + BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator); + + buffer[24] = (byte)this.DisposeOperation; + buffer[25] = (byte)this.BlendOperation; + } + + /// + /// Parses the APngFrameControl from the given data buffer. + /// + /// The data to parse. + /// The parsed fcTL. + public static APngFrameControl Parse(ReadOnlySpan data) + => new( + sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]), + height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]), + xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]), + yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]), + delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]), + delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]), + disposeOperation: (APngDisposeOperation)data[24], + blendOperation: (APngBlendOperation)data[25]); +} diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs similarity index 98% rename from src/ImageSharp/Formats/Png/PngHeader.cs rename to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs index 06fec86f30..77fb706f60 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; -namespace SixLabors.ImageSharp.Formats.Png; +namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// Represents the png header chunk. diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs similarity index 89% rename from src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs rename to src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs index 34d53f00eb..7847882484 100644 --- a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers.Binary; @@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// -internal readonly struct PhysicalChunkData +internal readonly struct PngPhysical { public const int Size = 9; - public PhysicalChunkData(uint x, uint y, byte unitSpecifier) + public PngPhysical(uint x, uint y, byte unitSpecifier) { this.XAxisPixelsPerUnit = x; this.YAxisPixelsPerUnit = y; @@ -44,13 +44,13 @@ internal readonly struct PhysicalChunkData /// /// The data buffer. /// The parsed PhysicalChunkData. - public static PhysicalChunkData Parse(ReadOnlySpan data) + public static PngPhysical Parse(ReadOnlySpan data) { uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); byte unit = data[8]; - return new PhysicalChunkData(hResolution, vResolution, unit); + return new PngPhysical(hResolution, vResolution, unit); } /// @@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData /// /// The metadata. /// The constructed PngPhysicalChunkData instance. - public static PhysicalChunkData FromMetadata(ImageMetadata meta) + public static PngPhysical FromMetadata(ImageMetadata meta) { byte unitSpecifier = 0; uint x; @@ -92,7 +92,7 @@ internal readonly struct PhysicalChunkData break; } - return new PhysicalChunkData(x, y, unitSpecifier); + return new PngPhysical(x, y, unitSpecifier); } /// diff --git a/src/ImageSharp/Formats/Png/PngTextData.cs b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs similarity index 99% rename from src/ImageSharp/Formats/Png/PngTextData.cs rename to src/ImageSharp/Formats/Png/Chunks/PngTextData.cs index 8ef4f1821d..077eb46082 100644 --- a/src/ImageSharp/Formats/Png/PngTextData.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Png; +namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// Stores text data contained in the iTXt, tEXt, and zTXt chunks. diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs index e05bd5f844..0ae180e08d 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; @@ -14,7 +15,23 @@ public static partial class MetadataExtensions /// /// Gets the png format specific metadata for the image. /// - /// The metadata this method extends. + /// The metadata this method extends. /// The . - public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance); + public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The . + public static APngFrameMetadata GetAPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The metadata. + /// The . + public static bool TryGetAPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out APngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); + } diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index b514011eb3..e5fa5fbb72 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -42,7 +42,8 @@ internal readonly struct PngChunk /// Gets a value indicating whether the given chunk is critical to decoding /// public bool IsCritical => - this.Type == PngChunkType.Header || - this.Type == PngChunkType.Palette || - this.Type == PngChunkType.Data; + this.Type is PngChunkType.Header or + PngChunkType.Palette or + PngChunkType.Data or + PngChunkType.FrameData; } diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index f47c2e7f86..2c835bf8ca 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -8,16 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal enum PngChunkType : uint { + /// + /// + /// acTL + AnimationControl = 0x6163544cU, + + /// + /// + /// fcTL + FrameControl = 0x6663544cU, + + /// + /// + /// fdAT + FrameData = 0x66644154U, + /// /// The IDAT chunk contains the actual image data. The image can contains more /// than one chunk of this type. All chunks together are the whole image. /// + /// IDAT Data = 0x49444154U, /// /// This chunk must appear last. It marks the end of the PNG data stream. /// The chunk's data field is empty. /// + /// IEND End = 0x49454E44U, /// @@ -25,34 +42,40 @@ internal enum PngChunkType : uint /// common information like the width and the height of the image or /// the used compression method. /// + /// IHDR Header = 0x49484452U, /// /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// series in the RGB format. /// + /// PLTE Palette = 0x504C5445U, /// /// The eXIf data chunk which contains the Exif profile. /// + /// eXIF Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// + /// gAMA Gamma = 0x67414D41U, /// - /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. + /// This chunk specifies the intended pixel size or aspect ratio for display of the image. /// + /// pHYs Physical = 0x70485973U, /// /// Textual information that the encoder wishes to record with the image can be stored in /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. /// + /// tEXT Text = 0x74455874U, /// @@ -60,70 +83,82 @@ internal enum PngChunkType : uint /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and /// a compressed text string. /// + /// zTXt CompressedText = 0x7A545874U, /// - /// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword + /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword /// and the actual text string, which can be compressed or uncompressed. /// + /// iTXt InternationalText = 0x69545874U, /// - /// The tRNS chunk specifies that the image uses simple transparency: + /// This chunk specifies that the image uses simple transparency: /// either alpha values associated with palette entries (for indexed-color images) /// or a single transparent color (for grayscale and true color images). /// + /// tRNS Transparency = 0x74524E53U, /// - /// The tIME chunk gives the time of the last image modification (not the time of initial image creation). + /// This chunk gives the time of the last image modification (not the time of initial image creation). /// + /// tIME Time = 0x74494d45, /// - /// The bKGD chunk specifies a default background colour to present the image against. + /// This chunk specifies a default background colour to present the image against. /// If there is any other preferred background, either user-specified or part of a larger page (as in a browser), /// the bKGD chunk should be ignored. /// + /// bKGD Background = 0x624b4744, /// - /// The iCCP chunk contains a embedded color profile. If the iCCP chunk is present, + /// This chunk contains a embedded color profile. If the iCCP chunk is present, /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium. /// + /// iCCP EmbeddedColorProfile = 0x69434350, /// - /// The sBIT chunk defines the original number of significant bits (which can be less than or equal to the sample depth). + /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth). /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG. /// + /// sBIT SignificantBits = 0x73424954, /// - /// If the sRGB chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed + /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed /// using the specified rendering intent defined by the International Color Consortium. /// + /// sRGB StandardRgbColourSpace = 0x73524742, /// - /// The hIST chunk gives the approximate usage frequency of each colour in the palette. + /// This chunk gives the approximate usage frequency of each colour in the palette. /// + /// hIST Histogram = 0x68495354, /// - /// The sPLT chunk contains the suggested palette. + /// This chunk contains the suggested palette. /// + /// sPLT SuggestedPalette = 0x73504c54, /// - /// The cHRM chunk may be used to specify the 1931 CIE x,y chromaticities of the red, + /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red, /// green, and blue display primaries used in the image, and the referenced white point. /// + /// cHRM Chroma = 0x6348524d, /// /// Malformed chunk named CgBI produced by apple, which is not conform to the specification. /// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410 /// + /// CgBI ProprietaryApple = 0x43674249 } diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index b76c73b9f2..7877f84bd8 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -28,12 +28,12 @@ internal static class PngConstants /// /// The list of mimetypes that equate to a Png. /// - public static readonly IEnumerable MimeTypes = new[] { "image/png" }; + public static readonly IEnumerable MimeTypes = new[] { "image/png", "image/apng" }; /// /// The list of file extensions that equate to a Png. /// - public static readonly IEnumerable FileExtensions = new[] { "png" }; + public static readonly IEnumerable FileExtensions = new[] { "png", "apng" }; /// /// The header bytes as a big-endian coded ulong. @@ -43,7 +43,7 @@ internal static class PngConstants /// /// The dictionary of available color types. /// - public static readonly Dictionary ColorTypes = new Dictionary + public static readonly Dictionary ColorTypes = new() { [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Rgb] = new byte[] { 8, 16 }, @@ -80,24 +80,5 @@ internal static class PngConstants /// /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// - public static ReadOnlySpan XmpKeyword => new byte[] - { - (byte)'X', - (byte)'M', - (byte)'L', - (byte)':', - (byte)'c', - (byte)'o', - (byte)'m', - (byte)'.', - (byte)'a', - (byte)'d', - (byte)'o', - (byte)'b', - (byte)'e', - (byte)'.', - (byte)'x', - (byte)'m', - (byte)'p' - }; + public static ReadOnlySpan XmpKeyword => "XML:com.adobe.xmp"u8; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index d1d29dca6b..fa94e6925c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Compression; using System.Runtime.CompilerServices; @@ -28,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal sealed class PngDecoderCore : IImageDecoderInternals { + /// + /// Indicate whether the file is a simple PNG. + /// + private bool isSimplePng; + /// /// The general decoder options. /// @@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private BufferedReadStream currentStream; + private BufferedReadStream currentStream = null!; /// /// The png header. /// private PngHeader header; + /// + /// The png animation control. + /// + private APngAnimationControl? animationControl; + /// /// The number of bytes per pixel. /// @@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The palette containing color information for indexed png's. /// - private byte[] palette; + private byte[] palette = null!; /// /// The palette containing alpha channel color information for indexed png's. /// - private byte[] paletteAlpha; + private byte[] paletteAlpha = null!; /// /// Previous scanline processed. /// - private IMemoryOwner previousScanline; + private IMemoryOwner previousScanline = null!; /// /// The current scanline that is being processed. /// - private IMemoryOwner scanline; - - /// - /// The index of the current scanline being processed. - /// - private int currentRow = Adam7.FirstRow[0]; - - /// - /// The current number of bytes read in the current scanline. - /// - private int currentRowBytesRead; + private IMemoryOwner scanline = null!; /// /// Gets or sets the png color type. @@ -148,7 +148,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; this.currentStream.Skip(8); - Image image = null; + Image? image = null; + APngFrameControl? lastFrameControl = null; + ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; try @@ -160,22 +162,84 @@ internal sealed class PngDecoderCore : IImageDecoderInternals switch (chunk.Type) { case PngChunkType.Header: + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } + this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.AnimationControl: + if (this.isSimplePng || this.animationControl is not null) + { + PngThrowHelper.ThrowInvalidAnimationControl(); + } + + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Gamma: ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; - case PngChunkType.Data: + case PngChunkType.FrameControl: + if (this.isSimplePng) + { + continue; + } + + currentFrame = null; + lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: if (image is null) { - this.InitializeImage(metadata, out image); + PngThrowHelper.ThrowMissingDefaultData(); } - this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken); + if (lastFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + if (currentFrame is null) + { + this.InitializeFrame(lastFrameControl.Value, image, out currentFrame); + } + + this.currentStream.Position += 4; + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + () => + { + int length = this.ReadNextDataChunk(); + if (this.ReadNextDataChunk() is 0) + { + return length; + } + + this.currentStream.Position += 4; // Skip sequence number + return length - 4; + }, + cancellationToken); + lastFrameControl = null; + break; + case PngChunkType.Data: + if (this.animationControl is null) + { + this.isSimplePng = true; + } + + if (image is null) + { + this.InitializeImage(metadata, lastFrameControl, out image); + } + this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, cancellationToken); + lastFrameControl = null; break; case PngChunkType.Palette: byte[] pal = new byte[chunk.Length]; @@ -249,6 +313,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; + APngFrameControl? lastFrameControl = null; Span buffer = stackalloc byte[20]; this.currentStream.Skip(8); @@ -264,6 +329,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.AnimationControl: + if (this.isSimplePng || this.animationControl is not null) + { + PngThrowHelper.ThrowInvalidAnimationControl(); + } + + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: if (this.colorMetadataOnly) { @@ -282,7 +355,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.FrameControl: + if (this.isSimplePng) + { + continue; + } + + lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: + if (this.colorMetadataOnly) + { + goto EOF; + } + + if (lastFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + // Skip sequence number + this.currentStream.Skip(4); + this.SkipChunkDataAndCrc(chunk); + break; case PngChunkType.Data: + if (this.animationControl is null) + { + this.isSimplePng = true; + } // Spec says tRNS must be before IDAT so safe to exit. if (this.colorMetadataOnly) @@ -365,9 +465,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } EOF: - if (this.header.Width == 0 && this.header.Height == 0) + if (this.header is { Width: 0, Height: 0 }) { - PngThrowHelper.ThrowNoHeader(); + PngThrowHelper.ThrowInvalidHeader(); } return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); @@ -398,7 +498,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The number of bits per value. /// The new array. /// The resulting array. - private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IMemoryOwner buffer) + private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner? buffer) { if (bits >= 8) { @@ -433,7 +533,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The data containing physical data. private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan data) { - PhysicalChunkData physicalChunk = PhysicalChunkData.Parse(data); + PngPhysical physicalChunk = PngPhysical.Parse(data); metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue ? PixelResolutionUnit.AspectRatio @@ -466,8 +566,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The type the pixels will be /// The metadata information for the image + /// The frame control information for the frame /// The image that we will populate - private void InitializeImage(ImageMetadata metadata, out Image image) + private void InitializeImage(ImageMetadata metadata, APngFrameControl? frameControl, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( @@ -476,6 +577,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.header.Height, metadata); + if (frameControl is { } control) + { + APngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAPngFrameMetadata(); + frameMetadata.FromChunk(control); + } + this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; @@ -490,6 +597,27 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } + /// + /// Initializes the image and various buffers needed for processing + /// + /// The type the pixels will be + /// The frame control information for the frame + /// The image that we will populate + private void InitializeFrame(APngFrameControl frameControl, Image image, out ImageFrame frame) + where TPixel : unmanaged, IPixel + { + frame = image.Frames.CreateFrame(); + + APngFrameMetadata frameMetadata = frame.Metadata.GetAPngFrameMetadata(); + + frameMetadata.FromChunk(frameControl); + + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + } + /// /// Calculates the correct number of bits per pixel for the given color type. /// @@ -553,18 +681,19 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Reads the scanlines within the image. /// /// The pixel format. - /// The png chunk containing the compressed scanline data. + /// The length of the chunk that containing the compressed scanline data. /// The pixel data. /// The png metadata + /// A delegate to get more data from the inner stream for . /// The cancellation token. - private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void ReadScanlines(int chunkLength, ImageFrame image, PngMetadata pngMetadata, Func getData, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk); - deframeStream.AllocateNewBytes(chunk.Length, true); - DeflateStream dataStream = deframeStream.CompressedStream; + using ZlibInflateStream deframeStream = new(this.currentStream, getData); + deframeStream.AllocateNewBytes(chunkLength, true); + DeflateStream dataStream = deframeStream.CompressedStream!; - if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) + if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken); } @@ -585,22 +714,25 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - while (this.currentRow < this.header.Height) + int currentRow = Adam7.FirstRow[0]; + int currentRowBytesRead = 0; + int height = image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height; + while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); Span scanlineSpan = this.scanline.GetSpan(); - while (this.currentRowBytesRead < this.bytesPerScanline) + while (currentRowBytesRead < this.bytesPerScanline) { - int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, this.bytesPerScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; switch ((FilterType)scanlineSpan[0]) { @@ -628,10 +760,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); + this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); - this.currentRow++; + ++currentRow; } } @@ -647,8 +779,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + int currentRow = Adam7.FirstRow[0]; + int currentRowBytesRead = 0; int pass = 0; int width = this.header.Width; + int height = this.header.Height; + if (image.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? frameMetadata)) + { + width = frameMetadata.Width; + height = frameMetadata.Height; + } + Buffer2D imageBuffer = image.PixelBuffer; while (true) { @@ -656,7 +797,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (numColumns == 0) { - pass++; + ++pass; // This pass contains no data; skip to next pass continue; @@ -664,21 +805,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (this.currentRow < this.header.Height) + while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); - while (this.currentRowBytesRead < bytesPerInterlaceScanline) + while (currentRowBytesRead < bytesPerInterlaceScanline) { - int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); @@ -709,12 +850,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow); + Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); - this.currentRow += Adam7.RowIncrement[pass]; + currentRow += Adam7.RowIncrement[pass]; } pass++; @@ -722,7 +863,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (pass < 7) { - this.currentRow = Adam7.FirstRow[pass]; + currentRow = Adam7.FirstRow[pass]; } else { @@ -736,19 +877,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Processes the de-filtered scanline filling the image pixel data /// /// The pixel format. + /// The index of the current scanline being processed. /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline(int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { - Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow); + Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( @@ -840,7 +982,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadOnlySpan trimmed = defilteredScanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( @@ -975,6 +1117,31 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } } + /// + /// Reads a animation control chunk from the data. + /// + /// The png metadata. + /// The containing data. + private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + this.animationControl = APngAnimationControl.Parse(data); + + pngMetadata.NumberPlays = this.animationControl.NumberPlays; + } + + /// + /// Reads a header chunk from the data. + /// + /// The containing data. + private APngFrameControl ReadFrameControlChunk(ReadOnlySpan data) + { + APngFrameControl fcTL = APngFrameControl.Parse(data); + + fcTL.Validate(this.header); + + return fcTL; + } + /// /// Reads a header chunk from the data. /// @@ -1062,7 +1229,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadOnlySpan compressedData = data[(zeroIndex + 2)..]; - if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed) + if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed) && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) { metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); @@ -1355,7 +1522,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { ReadOnlySpan compressedData = data[dataStartIdx..]; - if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed)) + if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed)) { pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); } @@ -1378,7 +1545,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The string encoding to use. /// The uncompressed value. /// The . - private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) + private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, [NotNullWhen(true)] out string? value) { if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData)) { @@ -1407,7 +1574,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (this.TryReadChunk(buffer, out PngChunk chunk)) { - if (chunk.Type == PngChunkType.Data) + if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData) { chunk.Data?.Dispose(); return chunk.Length; @@ -1461,7 +1628,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. - if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency) + if (this.colorMetadataOnly && type is not PngChunkType.Header and not PngChunkType.Transparency) { chunk = new PngChunk(length, type); @@ -1476,9 +1643,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.ValidateChunk(chunk, buffer); - // Restore the stream position for IDAT chunks, because it will be decoded later and + // Restore the stream position for IDAT and fdAT chunks, because it will be decoded later and // was only read to verifying the CRC is correct. - if (type == PngChunkType.Data) + if (type is PngChunkType.Data or PngChunkType.FrameData) { this.currentStream.Position = pos; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 175a9f777d..8fcd1721d3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -9,6 +9,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Memory; @@ -647,9 +648,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable return; } - PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); + PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size); + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size); } /// diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 2d1f2dcc7d..292f087f27 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// -public sealed class PngFormat : IImageFormat +public sealed class PngFormat : IImageFormat { private PngFormat() { @@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat /// public PngMetadata CreateDefaultFormatMetadata() => new(); + + /// + public APngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9ff3905fe1..9f874d5c9a 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -32,6 +34,7 @@ public class PngMetadata : IDeepCloneable this.TransparentL16 = other.TransparentL16; this.TransparentRgb24 = other.TransparentRgb24; this.TransparentRgb48 = other.TransparentRgb48; + this.NumberPlays = other.NumberPlays; for (int i = 0; i < other.TextData.Count; i++) { @@ -95,6 +98,11 @@ public class PngMetadata : IDeepCloneable /// public IList TextData { get; set; } = new List(); + /// + /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public int NumberPlays { get; set; } + /// public IDeepCloneable DeepClone() => new PngMetadata(this); } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 04a23308cc..caba887921 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 67da78e45b..78c243eeef 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -12,13 +12,22 @@ internal static class PngThrowHelper => throw new InvalidImageContentException(errorMessage, innerException); [DoesNotReturn] - public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk"); + public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); [DoesNotReturn] - public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk"); + public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk."); [DoesNotReturn] - public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk"); + public static void ThrowMissingDefaultData() => throw new InvalidImageContentException("APNG Image does not contain a default data chunk."); + + [DoesNotReturn] + public static void ThrowInvalidAnimationControl() => throw new InvalidImageContentException("APNG Image must contain a acTL chunk and it must be located before any IDAT and fdAT chunks."); + + [DoesNotReturn] + public static void ThrowMissingFrameControl() => throw new InvalidImageContentException("One of APNG Image's frames do not have a frame control chunk."); + + [DoesNotReturn] + public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk."); [DoesNotReturn] public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); @@ -30,7 +39,7 @@ internal static class PngThrowHelper public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type"); + public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type."); [DoesNotReturn] public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 75d4b173c8..57608a9090 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,6 +13,7 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release + preview diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index d7a353665a..ff81401f56 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -2,7 +2,9 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -132,6 +134,34 @@ public class PngMetadataTests VerifyExifDataIsPresent(exif); } + [Theory] + [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)] + public void Decode_ReadsExifData2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + SkipMetadata = false + }; + + using Image image = provider.GetImage(PngDecoder.Instance, options); + TPixel pixel = image.Frames.RootFrame[5, 5]; + TPixel pixel2 = image.Frames[1][5, 5]; + } + + [Theory] + [WithFile(@"Png\pl.png", PixelTypes.Rgba32)] + public void Decode_ReadsExifData3(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + SkipMetadata = false + }; + + using Image image = provider.GetImage(PngDecoder.Instance, options); + } + [Theory] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 04341a2419..96b5b620b8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png.Chunks; namespace SixLabors.ImageSharp.Tests.Formats.Png; From 7b6c32d54b03351853f1605b18280e93cac4d73b Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 12 Aug 2023 12:47:34 +0800 Subject: [PATCH 02/44] 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 sealed class 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 @@ internal readonly struct APngFrameControl } } + /// + /// 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 @@ internal readonly struct APngFrameControl 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 class PngEncoder : QuantizingImageEncoder // 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 System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Memory; @@ -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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); - this.previousScanline = null; - this.currentScanline = null; + this.previousScanline = null!; + this.currentScanline = null!; } /// @@ -192,48 +215,44 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable ReadOnlySpan rowSpan, ref Span filter, ref Span attempt, - IndexedImageFrame quantized, + IndexedImageFrame? quantized, int row) where TPixel : unmanaged, IPixel { @@ -577,6 +593,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) { - if (this.interlaceMode == PngInterlaceMode.Adam7) + if (this.interlaceMode is PngInterlaceMode.Adam7) { - if (quantized != null) + if (quantized is not null) { this.EncodeAdam7IndexedPixels(quantized, deflateStream); } @@ -973,24 +1013,43 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // Store the chunks in repeated 64k blocks. // This reduces the memory load for decoding the image for many decoders. - int numChunks = bufferLength / MaxBlockSize; + int maxBlockSize = MaxBlockSize; + if (isFrame) + { + maxBlockSize -= 4; + } - if (bufferLength % MaxBlockSize != 0) + int numChunks = bufferLength / maxBlockSize; + + 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); + int length = bufferLength - (i * maxBlockSize); - if (length > 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { Span 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Interlaced encoding the pixels. /// /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable col < width; col += Adam7.ColumnIncrement[pass]) { - block[i++] = srcRow[col]; + block[i] = srcRow[col]; + ++i; } // Encode data @@ -1176,7 +1244,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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; 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 partial class PngDecoderTests 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 class PngMetadataTests VerifyExifDataIsPresent(exif); } - [Theory] - [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)] - public void Decode_ReadsExifData2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - DecoderOptions options = new() - { - SkipMetadata = false - }; - - using Image image = provider.GetImage(PngDecoder.Instance, options); - TPixel pixel = image.Frames.RootFrame[5, 5]; - TPixel pixel2 = image.Frames[1][5, 5]; - } - - [Theory] - [WithFile(@"Png\pl.png", PixelTypes.Rgba32)] - public void Decode_ReadsExifData3(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - DecoderOptions options = new() - { - SkipMetadata = false - }; - - using Image image = provider.GetImage(PngDecoder.Instance, options); - } - [Theory] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 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 TestImages 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/44] 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 partial class PngDecoderTests 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 partial class PngEncoderTests }); } + [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 class ImagingTestCaseUtility 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 class ImagingTestCaseUtility 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 class TestImageExtensions 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 class TestImageExtensions 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/44] 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 sealed class 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 @@ using System.Buffers.Binary; namespace SixLabors.ImageSharp.Formats.Png.Chunks; -internal record APngAnimationControl( +internal record AnimationControl( int NumberFrames, int NumberPlays) { @@ -36,7 +36,7 @@ internal record APngAnimationControl( /// /// 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 @@ using System.Buffers.Binary; 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 @@ internal readonly struct 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 @@ internal readonly struct 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 @@ internal readonly struct APngFrameControl /// /// 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 @@ internal readonly struct APngFrameControl /// /// 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 @@ internal readonly struct APngFrameControl 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: - if (this.isSimplePng) - { - continue; - } - lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: @@ -379,11 +349,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } EOF: - if (this.header is { Width: 0, Height: 0 }) + if (this.header.Width == 0 && this.header.Height == 0) { PngThrowHelper.ThrowInvalidHeader(); } @@ -568,7 +533,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); - ++currentRow; + currentRow++; } } @@ -784,7 +750,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (numColumns == 0) { - ++pass; + pass++; // This pass contains no data; skip to next pass continue; @@ -1124,7 +1090,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Reads a header chunk from the data. /// /// 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 class PngEncoder : QuantizingImageEncoder // 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { int width = this.width; int height = this.height; - if (pixels.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? pngMetadata)) + if (pixels.Metadata.TryGetAPngFrameMetadata(out PngFrameMetadata? pngMetadata)) { width = pngMetadata.Width; height = pngMetadata.Height; @@ -1095,7 +1094,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { Span 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ public sealed class PngFormat : IImageFormat 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 @@ public class APngFrameMetadata : IDeepCloneable /// /// 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 @@ public class APngFrameMetadata : IDeepCloneable } /// - 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/44] 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 @@ using System.Buffers.Binary; 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 @@ internal record AnimationControl( /// 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 @@ internal readonly struct FrameControl { 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { 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 sealed class PngDecoderCore : IImageDecoderInternals { 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 sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + uint frameCount = 0; ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; @@ -331,9 +351,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { int width = this.width; int height = this.height; - if (pixels.Metadata.TryGetAPngFrameMetadata(out PngFrameMetadata? pngMetadata)) + if (pixels.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? pngMetadata)) { width = pngMetadata.Width; height = pngMetadata.Height; diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index caba887921..125aa75b75 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -22,67 +22,16 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngThrowHelper [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/44] 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 @@ internal readonly struct 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 @@ internal readonly struct FrameControl 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.currentStream.Position += 4; // Skip sequence number return length - 4; }, + lastFrameControl.Value, cancellationToken); lastFrameControl = null; break; @@ -237,7 +238,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata); + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); currentRow++; @@ -769,23 +774,19 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, pngMetadata.HasTransparency, @@ -913,7 +916,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)this.bytesPerPixel, @@ -923,7 +927,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( - this.header, + frameControl, scanlineSpan, rowSpan, this.palette, @@ -933,8 +937,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( - this.configuration, - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -947,8 +951,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( - this.configuration, - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -967,12 +971,13 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Processes the interlaced de-filtered scanline filling the image pixel data /// /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1007,7 +1013,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1019,7 +1026,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - this.header, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1031,7 +1038,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1046,7 +1054,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { Span 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Interlaced encoding the pixels. /// /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal static class PngScanlineProcessor L8 luminanceTrans) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( - header, + bitDepth, + frameControl, scanlineSpan, rowSpan, 0, @@ -34,7 +36,8 @@ internal static class PngScanlineProcessor luminanceTrans); public static void ProcessInterlacedGrayscaleScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -44,17 +47,18 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor bytesPerSample); public static void ProcessInterlacedGrayscaleWithAlphaScanline( - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -130,15 +137,16 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor paletteAlpha); public static void ProcessInterlacedPaletteScanline( - in PngHeader header, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -193,6 +202,7 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor // 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor } public static void ProcessRgbScanline( - Configuration configuration, - in PngHeader header, + int bitDepth, + FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, @@ -240,16 +250,17 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor { 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/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } }); } - } /// From 316a8395b6b5e266d5b89b540592e09a6ec9e406 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 18 Aug 2023 10:07:46 +0800 Subject: [PATCH 08/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal static class PngScanlineProcessor public static void ProcessInterlacedGrayscaleScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -110,7 +110,7 @@ internal static class PngScanlineProcessor public static void ProcessGrayscaleWithAlphaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, @@ -128,7 +128,7 @@ internal static class PngScanlineProcessor public static void ProcessInterlacedGrayscaleWithAlphaScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -172,7 +172,7 @@ internal static class PngScanlineProcessor } public static void ProcessPaletteScanline( - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, ReadOnlySpan palette, @@ -188,7 +188,7 @@ internal static class PngScanlineProcessor paletteAlpha); public static void ProcessInterlacedPaletteScanline( - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -240,7 +240,7 @@ internal static class PngScanlineProcessor public static void ProcessRgbScanline( int bitDepth, - FrameControl frameControl, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, @@ -248,84 +248,23 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor } 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/44] 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); - this.previousScanline = null!; - this.currentScanline = null!; } /// From 3fabb76ab62b4412036de1a687dd38c9bea27928 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Oct 2023 21:32:33 +1000 Subject: [PATCH 10/44] Pool Vp8LHistogram memory. --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 47 +-- .../Formats/Webp/Lossless/CostModel.cs | 16 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 189 ++++++------ .../Formats/Webp/Lossless/HuffmanUtils.cs | 18 +- .../Formats/Webp/Lossless/PixOrCopy.cs | 2 +- .../Formats/Webp/Lossless/Vp8LBitEntropy.cs | 6 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 117 +++++--- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 273 +++++++++++------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 122 ++++++++ .../Codecs/Webp/EncodeWebp.cs | 13 +- .../Formats/WebP/DominantCostRangeTests.cs | 10 +- .../Formats/WebP/Vp8LHistogramTests.cs | 14 +- 12 files changed, 539 insertions(+), 288 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 61133142bf..922ae0193d 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -50,8 +50,8 @@ internal static class BackwardReferenceEncoder double bitCostBest = -1; int cacheBitsInitial = cacheBits; Vp8LHashChain? hashChainBox = null; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) { int cacheBitsTmp = cacheBitsInitial; @@ -76,21 +76,19 @@ internal static class BackwardReferenceEncoder } // Next, try with a color cache and update the references. - cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp); + cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, bgra, quality, worst, cacheBitsTmp); if (cacheBitsTmp > 0) { BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); } // Keep the best backward references. - var histo = new Vp8LHistogram(worst, cacheBitsTmp); + using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) { - Vp8LBackwardRefs tmp = worst; - worst = best; - best = tmp; + (best, worst) = (worst, best); bitCostBest = bitCost; cacheBits = cacheBitsTmp; lz77TypeBest = lz77Type; @@ -102,7 +100,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - var histo = new Vp8LHistogram(worst, cacheBits); + using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { @@ -123,7 +121,12 @@ internal static class BackwardReferenceEncoder /// The local color cache is also disabled for the lower (smaller then 25) quality. /// /// Best cache size. - private static int CalculateBestCacheSize(ReadOnlySpan bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits) + private static int CalculateBestCacheSize( + MemoryAllocator memoryAllocator, + ReadOnlySpan bgra, + uint quality, + Vp8LBackwardRefs refs, + int bestCacheBits) { int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits; if (cacheBitsMax == 0) @@ -134,11 +137,15 @@ internal static class BackwardReferenceEncoder double entropyMin = MaxEntropy; int pos = 0; - var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; - var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1]; + + // TODO: Pass from outer loop and clear. + ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; + + // TODO: Use fixed size. + using Vp8LHistogramSet histos = new(memoryAllocator, WebpConstants.MaxColorCacheBits + 1, 0); for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++) { - histos[i] = new Vp8LHistogram(paletteCodeBits: i); + histos[i].PaletteCodeBits = i; colorCache[i] = new ColorCache(i); } @@ -149,10 +156,10 @@ internal static class BackwardReferenceEncoder if (v.IsLiteral()) { uint pix = bgra[pos++]; - uint a = (pix >> 24) & 0xff; - uint r = (pix >> 16) & 0xff; - uint g = (pix >> 8) & 0xff; - uint b = (pix >> 0) & 0xff; + int a = (int)(pix >> 24) & 0xff; + int r = (int)(pix >> 16) & 0xff; + int g = (int)(pix >> 8) & 0xff; + int b = (int)(pix >> 0) & 0xff; // The keys of the caches can be derived from the longest one. int key = ColorCache.HashPix(pix, 32 - cacheBitsMax); @@ -218,8 +225,8 @@ internal static class BackwardReferenceEncoder } } - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int i = 0; i <= cacheBitsMax; i++) { double entropy = histos[i].EstimateBits(stats, bitsEntropy); @@ -266,7 +273,7 @@ internal static class BackwardReferenceEncoder int pixCount = xSize * ySize; bool useColorCache = cacheBits > 0; int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0); - var costModel = new CostModel(literalArraySize); + CostModel costModel = new(memoryAllocator, literalArraySize); int offsetPrev = -1; int lenPrev = -1; double offsetCost = -1; @@ -280,7 +287,7 @@ internal static class BackwardReferenceEncoder } costModel.Build(xSize, cacheBits, refs); - using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel); + using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel); Span costManagerCosts = costManager.Costs.GetSpan(); Span distArray = distArrayBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index c99e8fe6e2..975fd581d7 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -1,18 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal class CostModel { + private readonly MemoryAllocator memoryAllocator; private const int ValuesInBytes = 256; /// /// Initializes a new instance of the class. /// + /// The memory allocator. /// The literal array size. - public CostModel(int literalArraySize) + public CostModel(MemoryAllocator memoryAllocator, int literalArraySize) { + this.memoryAllocator = memoryAllocator; this.Alpha = new double[ValuesInBytes]; this.Red = new double[ValuesInBytes]; this.Blue = new double[ValuesInBytes]; @@ -32,13 +37,12 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - var histogram = new Vp8LHistogram(cacheBits); - using System.Collections.Generic.List.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator(); + using Vp8LHistogram histogram = new(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. - while (refsEnumerator.MoveNext()) + for (int i = 0; i < backwardRefs.Refs.Count; i++) { - histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize); + histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize); } ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); @@ -70,7 +74,7 @@ internal class CostModel public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff]; - private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output) + private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span populationCounts, double[] output) { uint sum = 0; int nonzeros = 0; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index dd59ed2097..8a0d132063 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -3,6 +3,7 @@ #nullable disable using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; @@ -27,19 +28,28 @@ internal static class HistogramEncoder private const ushort InvalidHistogramSymbol = ushort.MaxValue; - public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, Span histogramSymbols) + public static void GetHistoImageSymbols( + MemoryAllocator memoryAllocator, + int xSize, + int ySize, + Vp8LBackwardRefs refs, + uint quality, + int histoBits, + int cacheBits, + Vp8LHistogramSet imageHisto, + Vp8LHistogram tmpHisto, + Span histogramSymbols) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; int imageHistoRawSize = histoXSize * histoYSize; - int entropyCombineNumBins = BinSize; + const int entropyCombineNumBins = BinSize; + + // TODO: Allocations! ushort[] mapTmp = new ushort[imageHistoRawSize]; ushort[] clusterMappings = new ushort[imageHistoRawSize]; - var origHisto = new List(imageHistoRawSize); - for (int i = 0; i < imageHistoRawSize; i++) - { - origHisto.Add(new Vp8LHistogram(cacheBits)); - } + + using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits); // Construct the histograms from the backward references. HistogramBuild(xSize, histoBits, refs, origHisto); @@ -61,7 +71,7 @@ internal static class HistogramEncoder OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); } - float x = quality / 100.0f; + float x = quality / 100F; // Cubic ramp between 1 and MaxHistoGreedy: int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1))); @@ -77,26 +87,25 @@ internal static class HistogramEncoder HistogramRemap(origHisto, imageHisto, histogramSymbols); } - private static void RemoveEmptyHistograms(List histograms) + private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms) { - int size = 0; - for (int i = 0; i < histograms.Count; i++) + for (int i = histograms.Count - 1; i >= 0; i--) { if (histograms[i] == null) { - continue; + histograms.RemoveAt(i); } - - histograms[size++] = histograms[i]; } - - histograms.RemoveRange(size, histograms.Count - size); } /// /// Construct the histograms from the backward references. /// - private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List histograms) + private static void HistogramBuild( + int xSize, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Vp8LHistogramSet histograms) { int x = 0, y = 0; int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); @@ -119,10 +128,10 @@ internal static class HistogramEncoder /// Partition histograms to different entropy bins for three dominant (literal, /// red and blue) symbol costs and compute the histogram aggregate bitCost. /// - private static void HistogramAnalyzeEntropyBin(List histograms, ushort[] binMap) + private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, ushort[] binMap) { int histoSize = histograms.Count; - var costRange = new DominantCostRange(); + DominantCostRange costRange = new(); // Analyze the dominant (literal, red and blue) entropy costs. for (int i = 0; i < histoSize; i++) @@ -148,25 +157,28 @@ internal static class HistogramEncoder } } - private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, Span histogramSymbols) + private static int HistogramCopyAndAnalyze( + Vp8LHistogramSet origHistograms, + Vp8LHistogramSet histograms, + Span histogramSymbols) { - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int clusterId = 0, i = 0; i < origHistograms.Count; i++) { Vp8LHistogram origHistogram = origHistograms[i]; origHistogram.UpdateHistogramCost(stats, bitsEntropy); // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). - if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4]) + if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4)) { - origHistograms[i] = null; - histograms[i] = null; + origHistograms.DisposeAt(i); + histograms.DisposeAt(i); histogramSymbols[i] = InvalidHistogramSymbol; } else { - histograms[i] = (Vp8LHistogram)origHistogram.DeepClone(); + origHistogram.CopyTo(histograms[i]); histogramSymbols[i] = (ushort)clusterId++; } } @@ -184,7 +196,7 @@ internal static class HistogramEncoder } private static void HistogramCombineEntropyBin( - List histograms, + Vp8LHistogramSet histograms, Span clusters, ushort[] clusterMappings, Vp8LHistogram curCombo, @@ -205,9 +217,9 @@ internal static class HistogramEncoder clusterMappings[idx] = (ushort)idx; } - var indicesToRemove = new List(); - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + List indicesToRemove = new(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int idx = 0; idx < histograms.Count; idx++) { if (histograms[idx] == null) @@ -236,15 +248,13 @@ internal static class HistogramEncoder // histogram pairs. In that case, we fallback to combining // histograms as usual to avoid increasing the header size. bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym); - int maxCombineFailures = 32; + const int maxCombineFailures = 32; if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures) { // Move the (better) merged histogram to its final slot. - Vp8LHistogram tmp = curCombo; - curCombo = histograms[first]; - histograms[first] = tmp; + (histograms[first], curCombo) = (curCombo, histograms[first]); - histograms[idx] = null; + histograms.DisposeAt(idx); indicesToRemove.Add(idx); clusterMappings[clusters[idx]] = clusters[first]; } @@ -256,9 +266,9 @@ internal static class HistogramEncoder } } - foreach (int index in indicesToRemove.OrderByDescending(i => i)) + for (int i = indicesToRemove.Count - 1; i >= 0; i--) { - histograms.RemoveAt(index); + histograms.RemoveAt(indicesToRemove[i]); } } @@ -318,15 +328,15 @@ internal static class HistogramEncoder /// Perform histogram aggregation using a stochastic approach. /// /// true if a greedy approach needs to be performed afterwards, false otherwise. - private static bool HistogramCombineStochastic(List histograms, int minClusterSize) + private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize) { uint seed = 1; int triesWithNoSuccess = 0; int numUsed = histograms.Count(h => h != null); int outerIters = numUsed; int numTriesNoSuccess = (int)((uint)outerIters / 2); - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); if (numUsed < minClusterSize) { @@ -335,25 +345,25 @@ internal static class HistogramEncoder // Priority list of histogram pairs. Its size impacts the quality of the compression and the speed: // the smaller the faster but the worse for the compression. - var histoPriorityList = new List(); - int maxSize = 9; + List histoPriorityList = new(); + const int maxSize = 9; // Fill the initial mapping. Span mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count]; - for (int j = 0, iter = 0; iter < histograms.Count; iter++) + for (int j = 0, i = 0; i < histograms.Count; i++) { - if (histograms[iter] == null) + if (histograms[i] == null) { continue; } - mappings[j++] = iter; + mappings[j++] = i; } // Collapse similar histograms. - for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++) + for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++) { - double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff; + double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff; int numTries = (int)((uint)numUsed / 2); uint randRange = (uint)((numUsed - 1) * numUsed); @@ -373,7 +383,8 @@ internal static class HistogramEncoder idx2 = mappings[idx2]; // Calculate cost reduction on combination. - double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); + double currCost = 0; + currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); // Found a better pair? if (currCost < 0) @@ -398,13 +409,13 @@ internal static class HistogramEncoder int mappingIndex = mappings.IndexOf(bestIdx2); Span src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1); - Span dst = mappings.Slice(mappingIndex); + Span dst = mappings[mappingIndex..]; src.CopyTo(dst); // Merge the histograms and remove bestIdx2 from the list. HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); - histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo; - histograms[bestIdx2] = null; + histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo; + histograms.DisposeAt(bestIdx2); numUsed--; for (int j = 0; j < histoPriorityList.Count;) @@ -418,7 +429,7 @@ internal static class HistogramEncoder // check for it all the time nevertheless. if (isIdx1Best && isIdx2Best) { - histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[j] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); continue; } @@ -439,18 +450,17 @@ internal static class HistogramEncoder // Make sure the index order is respected. if (p.Idx1 > p.Idx2) { - int tmp = p.Idx2; - p.Idx2 = p.Idx1; - p.Idx1 = tmp; + (p.Idx1, p.Idx2) = (p.Idx2, p.Idx1); } if (doEval) { // Re-evaluate the cost of an updated pair. - HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0.0d, p); - if (p.CostDiff >= 0.0d) + HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p); + + if (p.CostDiff >= 0D) { - histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[j] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); continue; } @@ -463,20 +473,18 @@ internal static class HistogramEncoder triesWithNoSuccess = 0; } - bool doGreedy = numUsed <= minClusterSize; - - return doGreedy; + return numUsed <= minClusterSize; } - private static void HistogramCombineGreedy(List histograms) + private static void HistogramCombineGreedy(Vp8LHistogramSet histograms) { int histoSize = histograms.Count(h => h != null); // Priority list of histogram pairs. - var histoPriorityList = new List(); + List histoPriorityList = new(); int maxSize = histoSize * histoSize; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int i = 0; i < histoSize; i++) { @@ -504,16 +512,17 @@ internal static class HistogramEncoder histograms[idx1].BitCost = histoPriorityList[0].CostCombo; // Remove merged histogram. - histograms[idx2] = null; + histograms.DisposeAt(idx2); // Remove pairs intersecting the just combined best pair. + // TODO: Reversing this will avoid the need to remove from the end of the list. for (int i = 0; i < histoPriorityList.Count;) { - HistogramPair p = histoPriorityList.ElementAt(i); + HistogramPair p = histoPriorityList[i]; if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2) { // Replace item at pos i with the last one and shrinking the list. - histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[i] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); } else @@ -536,12 +545,15 @@ internal static class HistogramEncoder } } - private static void HistogramRemap(List input, List output, Span symbols) + private static void HistogramRemap( + Vp8LHistogramSet input, + Vp8LHistogramSet output, + Span symbols) { int inSize = input.Count; int outSize = output.Count; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); if (outSize > 1) { for (int i = 0; i < inSize; i++) @@ -577,11 +589,11 @@ internal static class HistogramEncoder } // Recompute each output. - int paletteCodeBits = output.First().PaletteCodeBits; - output.Clear(); + int paletteCodeBits = output[0].PaletteCodeBits; for (int i = 0; i < outSize; i++) { - output.Add(new Vp8LHistogram(paletteCodeBits)); + output[i].Clear(); + output[i].PaletteCodeBits = paletteCodeBits; } for (int i = 0; i < inSize; i++) @@ -600,20 +612,26 @@ internal static class HistogramEncoder /// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy. /// /// The cost of the pair, or 0 if it superior to threshold. - private static double HistoPriorityListPush(List histoList, int maxSize, List histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy) + private static double HistoPriorityListPush( + List histoList, + int maxSize, + Vp8LHistogramSet histograms, + int idx1, + int idx2, + double threshold, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy) { - var pair = new HistogramPair(); + HistogramPair pair = new(); if (histoList.Count == maxSize) { - return 0.0d; + return 0D; } if (idx1 > idx2) { - int tmp = idx2; - idx2 = idx1; - idx1 = tmp; + (idx1, idx2) = (idx2, idx1); } pair.Idx1 = idx1; @@ -637,9 +655,16 @@ internal static class HistogramEncoder } /// - /// Update the cost diff and combo of a pair of histograms. This needs to be called when the the histograms have been merged with a third one. + /// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been + /// merged with a third one. /// - private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double threshold, HistogramPair pair) + private static void HistoListUpdatePair( + Vp8LHistogram h1, + Vp8LHistogram h2, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy, + double threshold, + HistogramPair pair) { double sumCost = h1.BitCost + h2.BitCost; pair.CostCombo = 0.0d; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs index 39ad967e38..027d4f7ee9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs @@ -25,7 +25,7 @@ internal static class HuffmanUtils 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf }; - public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) + public static void CreateHuffmanTree(Span histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) { int numSymbols = huffCode.NumSymbols; bufRle.AsSpan().Clear(); @@ -40,7 +40,7 @@ internal static class HuffmanUtils /// Change the population counts in a way that the consequent /// Huffman tree compression, especially its RLE-part, give smaller output. /// - public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts) + public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span counts) { // 1) Let's make the Huffman code more compatible with rle encoding. for (; length >= 0; --length) @@ -116,7 +116,7 @@ internal static class HuffmanUtils { // We don't want to change value at counts[i], // that is already belonging to the next stride. Thus - 1. - counts[i - k - 1] = count; + counts[(int)(i - k - 1)] = count; } } @@ -159,7 +159,7 @@ internal static class HuffmanUtils /// The size of the histogram. /// The tree depth limit. /// How many bits are used for the symbol. - public static void GenerateOptimalTree(Span tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) + public static void GenerateOptimalTree(Span tree, Span histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) { uint countMin; int treeSizeOrig = 0; @@ -177,7 +177,7 @@ internal static class HuffmanUtils return; } - Span treePool = tree.Slice(treeSizeOrig); + Span treePool = tree[treeSizeOrig..]; // For block sizes with less than 64k symbols we never need to do a // second iteration of this loop. @@ -202,7 +202,7 @@ internal static class HuffmanUtils } // Build the Huffman tree. - Span treeSlice = tree.Slice(0, treeSize); + Span treeSlice = tree[..treeSize]; treeSlice.Sort(HuffmanTree.Compare); if (treeSize > 1) @@ -357,7 +357,7 @@ internal static class HuffmanUtils // Special case code with only one value. if (offsets[WebpConstants.MaxAllowedCodeLength] == 1) { - var huffmanCode = new HuffmanCode() + HuffmanCode huffmanCode = new() { BitsUsed = 0, Value = (uint)sorted[0] @@ -390,7 +390,7 @@ internal static class HuffmanUtils for (; countsLen > 0; countsLen--) { - var huffmanCode = new HuffmanCode() + HuffmanCode huffmanCode = new() { BitsUsed = len, Value = (uint)sorted[symbol++] @@ -432,7 +432,7 @@ internal static class HuffmanUtils }; } - var huffmanCode = new HuffmanCode + HuffmanCode huffmanCode = new() { BitsUsed = len - rootBits, Value = (uint)sorted[symbol++] diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index 6a28e5b3fb..cedc809382 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -37,7 +37,7 @@ internal sealed class PixOrCopy Len = len }; - public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff; + public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF; public uint CacheIdx() => this.BgraOrDistance; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs index 649845b025..330d1c555e 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs @@ -125,7 +125,7 @@ internal class Vp8LBitEntropy /// /// Get the entropy for the distribution 'X'. /// - public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + public void BitsEntropyUnrefined(Span x, int length, Vp8LStreaks stats) { int i; int iPrev = 0; @@ -147,7 +147,7 @@ internal class Vp8LBitEntropy this.Entropy += LosslessUtils.FastSLog2(this.Sum); } - public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats) + public void GetCombinedEntropyUnrefined(Span x, Span y, int length, Vp8LStreaks stats) { int i; int iPrev = 0; @@ -169,7 +169,7 @@ internal class Vp8LBitEntropy this.Entropy += LosslessUtils.FastSLog2(this.Sum); } - public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + public void GetEntropyUnrefined(Span x, int length, Vp8LStreaks stats) { int i; int iPrev = 0; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 1f7c7586eb..10fc8ab804 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,15 +589,21 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new(cacheBits); - List histogramImage = new(histogramImageXySize); - for (int i = 0; i < histogramImageXySize; i++) - { - histogramImage.Add(new Vp8LHistogram(cacheBits)); - } + using Vp8LHistogram tmpHisto = new(this.memoryAllocator, cacheBits); + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. - HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols); + HistogramEncoder.GetHistoImageSymbols( + this.memoryAllocator, + width, + height, + refsBest, + this.quality, + this.HistoBits, + cacheBits, + histogramImage, + tmpHisto, + histogramSymbols); // Create Huffman bit lengths and codes for each histogram image. int histogramImageSize = histogramImage.Count; @@ -678,9 +684,7 @@ internal class Vp8LEncoder : IDisposable // Keep track of the smallest image so far. if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) { - Vp8LBitWriter tmp = this.bitWriter; - this.bitWriter = bitWriterBest; - bitWriterBest = tmp; + (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest); } isFirstIteration = false; @@ -787,13 +791,8 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new() - { - new(cacheBits) - }; - // Build histogram image and symbols from backward references. - histogramImage[0].StoreRefs(refs); + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits); // Create Huffman bit lengths and codes for each histogram image. GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); @@ -833,7 +832,7 @@ internal class Vp8LEncoder : IDisposable private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) { int count = 0; - Span symbols = this.scratch.Span.Slice(0, 2); + Span symbols = this.scratch.Span[..2]; symbols.Clear(); const int maxBits = 8; const int maxSymbol = 1 << maxBits; @@ -886,6 +885,7 @@ internal class Vp8LEncoder : IDisposable private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) { + // TODO: Allocations. int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; @@ -996,7 +996,12 @@ internal class Vp8LEncoder : IDisposable } } - private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span histogramSymbols, HuffmanTreeCode[] huffmanCodes) + private void StoreImageToBitMask( + int width, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Span histogramSymbols, + HuffmanTreeCode[] huffmanCodes) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); @@ -1008,10 +1013,10 @@ internal class Vp8LEncoder : IDisposable int tileY = y & tileMask; int histogramIx = histogramSymbols[0]; Span codes = huffmanCodes.AsSpan(5 * histogramIx); - using List.Enumerator c = backwardRefs.Refs.GetEnumerator(); - while (c.MoveNext()) + + for (int i = 0; i < backwardRefs.Refs.Count; i++) { - PixOrCopy v = c.Current; + PixOrCopy v = backwardRefs.Refs[i]; if (tileX != (x & tileMask) || tileY != (y & tileMask)) { tileX = x & tileMask; @@ -1024,7 +1029,7 @@ internal class Vp8LEncoder : IDisposable { for (int k = 0; k < 4; k++) { - int code = (int)v.Literal(Order[k]); + int code = v.Literal(Order[k]); this.bitWriter.WriteHuffmanCode(codes[k], code); } } @@ -1379,10 +1384,8 @@ internal class Vp8LEncoder : IDisposable useLut = false; break; } - else - { - buffer[ind] = (uint)j; - } + + buffer[ind] = (uint)j; } if (useLut) @@ -1591,14 +1594,12 @@ internal class Vp8LEncoder : IDisposable } // Swap color(palette[bestIdx], palette[i]); - uint best = palette[bestIdx]; - palette[bestIdx] = palette[i]; - palette[i] = best; + (palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]); predict = palette[i]; } } - private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes) + private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes) { int maxNumSymbols = 0; @@ -1609,9 +1610,20 @@ internal class Vp8LEncoder : IDisposable int startIdx = 5 * i; for (int k = 0; k < 5; k++) { - int numSymbols = - k == 0 ? histo.NumCodes() : - k == 4 ? WebpConstants.NumDistanceCodes : 256; + int numSymbols; + if (k == 0) + { + numSymbols = histo.NumCodes(); + } + else if (k == 4) + { + numSymbols = WebpConstants.NumDistanceCodes; + } + else + { + numSymbols = 256; + } + huffmanCodes[startIdx + k].NumSymbols = numSymbols; } } @@ -1629,6 +1641,7 @@ internal class Vp8LEncoder : IDisposable } // Create Huffman trees. + // TODO: Allocations. Size here has a max and can be sliced. bool[] bufRle = new bool[maxNumSymbols]; Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols]; @@ -1682,8 +1695,18 @@ internal class Vp8LEncoder : IDisposable histoBits++; } - return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits : - histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits; + if (histoBits < WebpConstants.MinHuffmanBits) + { + return WebpConstants.MinHuffmanBits; + } + else if (histoBits > WebpConstants.MaxHuffmanBits) + { + return WebpConstants.MaxHuffmanBits; + } + else + { + return histoBits; + } } /// @@ -1720,11 +1743,7 @@ internal class Vp8LEncoder : IDisposable [MethodImpl(InliningOptions.ShortMethod)] private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst) - { - Vp8LBitWriter tmp = src; - src = dst; - dst = tmp; - } + => (dst, src) = (src, dst); /// /// Calculates the bits used for the transformation. @@ -1732,9 +1751,21 @@ internal class Vp8LEncoder : IDisposable [MethodImpl(InliningOptions.ShortMethod)] private static int GetTransformBits(WebpEncodingMethod method, int histoBits) { - int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5; - int res = histoBits > maxTransformBits ? maxTransformBits : histoBits; - return res; + int maxTransformBits; + if ((int)method < 4) + { + maxTransformBits = 6; + } + else if (method > WebpEncodingMethod.Level4) + { + maxTransformBits = 4; + } + else + { + maxTransformBits = 5; + } + + return histoBits > maxTransformBits ? maxTransformBits : histoBits; } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 5ec3f0d53d..07ee88f259 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -1,63 +1,73 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed class Vp8LHistogram : IDeepCloneable +internal sealed class Vp8LHistogram : IDisposable { private const uint NonTrivialSym = 0xffffffff; + private readonly IMemoryOwner buffer; + private const int RedSize = WebpConstants.NumLiteralCodes; + private const int BlueSize = WebpConstants.NumLiteralCodes; + private const int AlphaSize = WebpConstants.NumLiteralCodes; + private const int DistanceSize = WebpConstants.NumDistanceCodes; + public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1; + private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance + public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize; + private readonly bool isSetMember; /// /// Initializes a new instance of the class. /// - /// The histogram to create an instance from. - private Vp8LHistogram(Vp8LHistogram other) - : this(other.PaletteCodeBits) + /// The memory allocator. + /// The backward references to initialize the histogram with. + /// The palette code bits. + public Vp8LHistogram(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(memoryAllocator, paletteCodeBits) => this.StoreRefs(refs); + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The palette code bits. + public Vp8LHistogram(MemoryAllocator memoryAllocator, int paletteCodeBits) { - other.Red.AsSpan().CopyTo(this.Red); - other.Blue.AsSpan().CopyTo(this.Blue); - other.Alpha.AsSpan().CopyTo(this.Alpha); - other.Literal.AsSpan().CopyTo(this.Literal); - other.Distance.AsSpan().CopyTo(this.Distance); - other.IsUsed.AsSpan().CopyTo(this.IsUsed); - this.LiteralCost = other.LiteralCost; - this.RedCost = other.RedCost; - this.BlueCost = other.BlueCost; - this.BitCost = other.BitCost; - this.TrivialSymbol = other.TrivialSymbol; - this.PaletteCodeBits = other.PaletteCodeBits; + this.buffer = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + this.PaletteCodeBits = paletteCodeBits; } /// /// Initializes a new instance of the class. /// + /// + /// This constructor should be used when the histogram is a member of a . + /// + /// The backing buffer. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits) - : this(paletteCodeBits) => this.StoreRefs(refs); + public Vp8LHistogram(IMemoryOwner buffer, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(buffer, paletteCodeBits) => this.StoreRefs(refs); /// /// Initializes a new instance of the class. /// + /// + /// This constructor should be used when the histogram is a member of a . + /// + /// The backing buffer. /// The palette code bits. - public Vp8LHistogram(int paletteCodeBits) + public Vp8LHistogram(IMemoryOwner buffer, int paletteCodeBits) { + this.buffer = buffer; this.PaletteCodeBits = paletteCodeBits; - this.Red = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Blue = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Distance = new uint[WebpConstants.NumDistanceCodes]; - - int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits); - this.Literal = new uint[literalSize + 1]; - - // 5 for literal, red, blue, alpha, distance. - this.IsUsed = new bool[5]; + this.isSetMember = true; } /// @@ -85,22 +95,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// public double BlueCost { get; set; } - public uint[] Red { get; } + public Span Red => this.buffer.GetSpan()[..RedSize]; - public uint[] Blue { get; } + public Span Blue => this.buffer.GetSpan().Slice(RedSize, BlueSize); - public uint[] Alpha { get; } + public Span Alpha => this.buffer.GetSpan().Slice(RedSize + BlueSize, AlphaSize); - public uint[] Literal { get; } + public Span Distance => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize, DistanceSize); - public uint[] Distance { get; } + public Span Literal => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize, LiteralSize); public uint TrivialSymbol { get; set; } - public bool[] IsUsed { get; } + private Span IsUsedSpan => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize, UsedSize); + + public bool IsDisposed { get; set; } - /// - public IDeepCloneable DeepClone() => new Vp8LHistogram(this); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0; + + /// + /// Creates a copy of the given class. + /// + /// The histogram to copy to. + public void CopyTo(Vp8LHistogram other) + { + this.Red.CopyTo(other.Red); + this.Blue.CopyTo(other.Blue); + this.Alpha.CopyTo(other.Alpha); + this.Literal.CopyTo(other.Literal); + this.Distance.CopyTo(other.Distance); + this.IsUsedSpan.CopyTo(other.IsUsedSpan); + + other.LiteralCost = this.LiteralCost; + other.RedCost = this.RedCost; + other.BlueCost = this.BlueCost; + other.BitCost = this.BitCost; + other.TrivialSymbol = this.TrivialSymbol; + other.PaletteCodeBits = this.PaletteCodeBits; + } + + public void Clear() + { + this.buffer.Clear(); + this.PaletteCodeBits = 0; + this.BitCost = 0; + this.LiteralCost = 0; + this.RedCost = 0; + this.BlueCost = 0; + this.TrivialSymbol = 0; + } /// /// Collect all the references into a histogram (without reset). @@ -108,10 +155,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// The backward references. public void StoreRefs(Vp8LBackwardRefs refs) { - using List.Enumerator c = refs.Refs.GetEnumerator(); - while (c.MoveNext()) + for (int i = 0; i < refs.Refs.Count; i++) { - this.AddSinglePixOrCopy(c.Current, false); + this.AddSinglePixOrCopy(refs.Refs[i], false); } } @@ -163,12 +209,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable { uint notUsed = 0; return - PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) - + PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1], stats, bitsEntropy) - + PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2], stats, bitsEntropy) - + PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3], stats, bitsEntropy) - + PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) - + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes) + this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy) + + this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy) + + this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy) + + this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy) + + this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); } @@ -177,12 +223,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable uint alphaSym = 0, redSym = 0, blueSym = 0; uint notUsed = 0; - double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3], stats, bitsEntropy); - double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy); + double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); int numCodes = this.NumCodes(); - this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); - this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1], stats, bitsEntropy); - this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2], stats, bitsEntropy); + this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); + this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy); + this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy); this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost; if ((alphaSym | redSym | blueSym) == NonTrivialSym) { @@ -234,7 +280,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable for (int i = 0; i < 5; i++) { - output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i]; + output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i)); } output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol @@ -247,9 +293,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable bool trivialAtEnd = false; cost = costInitial; - cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false, stats, bitEntropy); + cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy); - cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); + cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); if (cost > costThreshold) { @@ -270,155 +316,158 @@ internal sealed class Vp8LHistogram : IDeepCloneable } } - cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false, stats, bitEntropy); + cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy); if (cost > costThreshold) { return false; } cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes); - if (cost > costThreshold) - { - return false; - } - - return true; + return cost <= costThreshold; } private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize) { - if (this.IsUsed[0]) + if (this.IsUsed(0)) { - if (b.IsUsed[0]) + if (b.IsUsed(0)) { AddVector(this.Literal, b.Literal, output.Literal, literalSize); } else { - this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + this.Literal[..literalSize].CopyTo(output.Literal); } } - else if (b.IsUsed[0]) + else if (b.IsUsed(0)) { - b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + b.Literal[..literalSize].CopyTo(output.Literal); } else { - output.Literal.AsSpan(0, literalSize).Clear(); + output.Literal[..literalSize].Clear(); } } private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[1]) + if (this.IsUsed(1)) { - if (b.IsUsed[1]) + if (b.IsUsed(1)) { AddVector(this.Red, b.Red, output.Red, size); } else { - this.Red.AsSpan(0, size).CopyTo(output.Red); + this.Red[..size].CopyTo(output.Red); } } - else if (b.IsUsed[1]) + else if (b.IsUsed(1)) { - b.Red.AsSpan(0, size).CopyTo(output.Red); + b.Red[..size].CopyTo(output.Red); } else { - output.Red.AsSpan(0, size).Clear(); + output.Red[..size].Clear(); } } private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[2]) + if (this.IsUsed(2)) { - if (b.IsUsed[2]) + if (b.IsUsed(2)) { AddVector(this.Blue, b.Blue, output.Blue, size); } else { - this.Blue.AsSpan(0, size).CopyTo(output.Blue); + this.Blue[..size].CopyTo(output.Blue); } } - else if (b.IsUsed[2]) + else if (b.IsUsed(2)) { - b.Blue.AsSpan(0, size).CopyTo(output.Blue); + b.Blue[..size].CopyTo(output.Blue); } else { - output.Blue.AsSpan(0, size).Clear(); + output.Blue[..size].Clear(); } } private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[3]) + if (this.IsUsed(3)) { - if (b.IsUsed[3]) + if (b.IsUsed(3)) { AddVector(this.Alpha, b.Alpha, output.Alpha, size); } else { - this.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + this.Alpha[..size].CopyTo(output.Alpha); } } - else if (b.IsUsed[3]) + else if (b.IsUsed(3)) { - b.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + b.Alpha[..size].CopyTo(output.Alpha); } else { - output.Alpha.AsSpan(0, size).Clear(); + output.Alpha[..size].Clear(); } } private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[4]) + if (this.IsUsed(4)) { - if (b.IsUsed[4]) + if (b.IsUsed(4)) { AddVector(this.Distance, b.Distance, output.Distance, size); } else { - this.Distance.AsSpan(0, size).CopyTo(output.Distance); + this.Distance[..size].CopyTo(output.Distance); } } - else if (b.IsUsed[4]) + else if (b.IsUsed(4)) { - b.Distance.AsSpan(0, size).CopyTo(output.Distance); + b.Distance[..size].CopyTo(output.Distance); } else { - output.Distance.AsSpan(0, size).Clear(); + output.Distance[..size].Clear(); } } - private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) + private static double GetCombinedEntropy( + Span x, + Span y, + int length, + bool isXUsed, + bool isYUsed, + bool trivialAtEnd, + Vp8LStreaks stats, + Vp8LBitEntropy bitEntropy) { stats.Clear(); bitEntropy.Init(); @@ -450,18 +499,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable bitEntropy.GetEntropyUnrefined(x, length, stats); } } + else if (isYUsed) + { + bitEntropy.GetEntropyUnrefined(y, length, stats); + } else { - if (isYUsed) - { - bitEntropy.GetEntropyUnrefined(y, length, stats); - } - else - { - stats.Counts[0] = 1; - stats.Streaks[0][length > 3 ? 1 : 0] = length; - bitEntropy.Init(); - } + stats.Counts[0] = 1; + stats.Streaks[0][length > 3 ? 1 : 0] = length; + bitEntropy.Init(); } return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); @@ -482,7 +528,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// /// Get the symbol entropy for the distribution 'population'. /// - private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) + private double PopulationCost(Span population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) { bitEntropy.Init(); stats.Clear(); @@ -491,7 +537,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym; // The histogram is used if there is at least one non-zero streak. - isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0; + this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0); return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); } @@ -556,4 +602,17 @@ internal sealed class Vp8LHistogram : IDeepCloneable } } } + + public void Dispose() + { + if (!this.IsDisposed) + { + if (!this.isSetMember) + { + this.buffer.Dispose(); + } + + this.IsDisposed = true; + } + } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs new file mode 100644 index 0000000000..0044c7376e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System.Buffers; +using System.Collections; +using System.Diagnostics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless; + +internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable +{ + private readonly IMemoryOwner buffer; + private readonly List items; + private bool isDisposed; + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); + this.items.Add(new Vp8LHistogram(owner, cacheBits)); + } + } + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); + this.items.Add(new Vp8LHistogram(owner, refs, cacheBits)); + } + } + + public Vp8LHistogramSet(int capacity) => this.items = new(capacity); + + public Vp8LHistogramSet() => this.items = new(); + + public int Count => this.items.Count; + + public Vp8LHistogram this[int index] + { + get => this.items[index]; + + // TODO: Should we check and throw for null? + set => this.items[index] = value; + } + + public void DisposeAt(int index) + { + this.CheckDisposed(); + + Vp8LHistogram item = this.items[index]; + item?.Dispose(); + this.items[index] = null; + } + + public void RemoveAt(int index) + { + this.CheckDisposed(); + + Vp8LHistogram item = this.items[index]; + item?.Dispose(); + this.items.RemoveAt(index); +#pragma warning disable IDE0059 // Unnecessary assignment of a value + item = null; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.buffer.Dispose(); + + foreach (Vp8LHistogram item in this.items) + { + item?.Dispose(); + } + + this.items.Clear(); + this.isDisposed = true; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + [Conditional("DEBUG")] + private void CheckDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); + + private sealed class SetItemMemoryOwner : IMemoryOwner + { + public SetItemMemoryOwner(Memory memory) => this.Memory = memory; + + public Memory Memory { get; } + + public void Dispose() + { + // Do nothing, the underlying memory is owned by the parent set. + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs index 65b4ae2c31..c5fa8d03da 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs @@ -43,9 +43,9 @@ public class EncodeWebp [Benchmark(Description = "Magick Webp Lossy")] public void MagickWebpLossy() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); - var defines = new WebPWriteDefines + WebPWriteDefines defines = new() { Lossless = false, Method = 4, @@ -65,7 +65,7 @@ public class EncodeWebp [Benchmark(Description = "ImageSharp Webp Lossy")] public void ImageSharpWebpLossy() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); this.webp.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy, @@ -80,8 +80,8 @@ public class EncodeWebp [Benchmark(Baseline = true, Description = "Magick Webp Lossless")] public void MagickWebpLossless() { - using var memoryStream = new MemoryStream(); - var defines = new WebPWriteDefines + using MemoryStream memoryStream = new(); + WebPWriteDefines defines = new() { Lossless = true, Method = 4, @@ -97,12 +97,13 @@ public class EncodeWebp [Benchmark(Description = "ImageSharp Webp Lossless")] public void ImageSharpWebpLossless() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); this.webp.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless, Method = WebpEncodingMethod.Level4, NearLossless = false, + Quality = 75, // This is equal to exact = false in libwebp, which is the default. TransparentColorMode = WebpTransparentColorMode.Clear diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 11c4bb62e7..80b41c5e4e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -11,7 +11,7 @@ public class DominantCostRangeTests [Fact] public void DominantCost_Constructor() { - var dominantCostRange = new DominantCostRange(); + DominantCostRange dominantCostRange = new(); Assert.Equal(0, dominantCostRange.LiteralMax); Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin); Assert.Equal(0, dominantCostRange.RedMax); @@ -24,8 +24,8 @@ public class DominantCostRangeTests public void UpdateDominantCostRange_Works() { // arrange - var dominantCostRange = new DominantCostRange(); - var histogram = new Vp8LHistogram(10) + DominantCostRange dominantCostRange = new(); + using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 10) { LiteralCost = 1.0d, RedCost = 2.0d, @@ -50,7 +50,7 @@ public class DominantCostRangeTests public void GetHistoBinIndex_Works(int partitions, int expectedIndex) { // arrange - var dominantCostRange = new DominantCostRange() + DominantCostRange dominantCostRange = new() { BlueMax = 253.4625, BlueMin = 109.0, @@ -59,7 +59,7 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - var histogram = new Vp8LHistogram(6) + using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 6) { LiteralCost = 247.0d, RedCost = 112.0d, diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index 39c3c89550..755b735452 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Webp; @@ -65,7 +66,7 @@ public class Vp8LHistogramTests // All remaining values are expected to be zero. literals.AsSpan().CopyTo(expectedLiterals); - var backwardRefs = new Vp8LBackwardRefs(pixelData.Length); + Vp8LBackwardRefs backwardRefs = new(pixelData.Length); for (int i = 0; i < pixelData.Length; i++) { backwardRefs.Add(new PixOrCopy() @@ -76,15 +77,16 @@ public class Vp8LHistogramTests }); } - var histogram0 = new Vp8LHistogram(backwardRefs, 3); - var histogram1 = new Vp8LHistogram(backwardRefs, 3); + MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; + using Vp8LHistogram histogram0 = new(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram1 = new(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { - histogram0.IsUsed[i] = true; - histogram1.IsUsed[i] = true; + histogram0.IsUsed(i, true); + histogram1.IsUsed(i, true); } - var output = new Vp8LHistogram(3); + using Vp8LHistogram output = new(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 63829e84377dcfd00b3ee43347d297977a355fd9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Oct 2023 22:18:48 +1000 Subject: [PATCH 11/44] Remove more allocations and add tasks. --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 23 ++++++++----------- .../Formats/Webp/Lossless/HistogramEncoder.cs | 23 +++++++++---------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 7 +++--- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 922ae0193d..a56fc0faca 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -52,6 +52,8 @@ internal static class BackwardReferenceEncoder Vp8LHashChain? hashChainBox = null; Vp8LStreaks stats = new(); Vp8LBitEntropy bitsEntropy = new(); + + ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) { int cacheBitsTmp = cacheBitsInitial; @@ -76,7 +78,7 @@ internal static class BackwardReferenceEncoder } // Next, try with a color cache and update the references. - cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, bgra, quality, worst, cacheBitsTmp); + cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp); if (cacheBitsTmp > 0) { BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); @@ -123,6 +125,7 @@ internal static class BackwardReferenceEncoder /// Best cache size. private static int CalculateBestCacheSize( MemoryAllocator memoryAllocator, + Span colorCache, ReadOnlySpan bgra, uint quality, Vp8LBackwardRefs refs, @@ -138,12 +141,8 @@ internal static class BackwardReferenceEncoder double entropyMin = MaxEntropy; int pos = 0; - // TODO: Pass from outer loop and clear. - ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; - - // TODO: Use fixed size. - using Vp8LHistogramSet histos = new(memoryAllocator, WebpConstants.MaxColorCacheBits + 1, 0); - for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++) + using Vp8LHistogramSet histos = new(memoryAllocator, colorCache.Length, 0); + for (int i = 0; i < colorCache.Length; i++) { histos[i].PaletteCodeBits = i; colorCache[i] = new ColorCache(i); @@ -448,12 +447,12 @@ internal static class BackwardReferenceEncoder int ix = useColorCache ? colorCache!.Contains(color) : -1; if (ix >= 0) { - double mul0 = 0.68; + const double mul0 = 0.68; costVal += costModel.GetCacheCost((uint)ix) * mul0; } else { - double mul1 = 0.82; + const double mul1 = 0.82; if (useColorCache) { colorCache!.Insert(color); @@ -700,10 +699,8 @@ internal static class BackwardReferenceEncoder bestLength = MaxLength; break; } - else - { - bestLength = currLength; - } + + bestLength = currLength; } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 8a0d132063..cce0356176 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. #nullable disable +using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; @@ -45,9 +46,9 @@ internal static class HistogramEncoder int imageHistoRawSize = histoXSize * histoYSize; const int entropyCombineNumBins = BinSize; - // TODO: Allocations! - ushort[] mapTmp = new ushort[imageHistoRawSize]; - ushort[] clusterMappings = new ushort[imageHistoRawSize]; + using IMemoryOwner tmp = memoryAllocator.Allocate(imageHistoRawSize * 2, AllocationOptions.Clean); + Span mapTmp = tmp.Slice(0, imageHistoRawSize); + Span clusterMappings = tmp.Slice(imageHistoRawSize, imageHistoRawSize); using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits); @@ -60,13 +61,12 @@ internal static class HistogramEncoder bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100; if (entropyCombine) { - ushort[] binMap = mapTmp; int numClusters = numUsed; double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality); - HistogramAnalyzeEntropyBin(imageHisto, binMap); + HistogramAnalyzeEntropyBin(imageHisto, mapTmp); // Collapse histograms with similar entropy. - HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor); + HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, mapTmp, entropyCombineNumBins, combineCostFactor); OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); } @@ -128,7 +128,7 @@ internal static class HistogramEncoder /// Partition histograms to different entropy bins for three dominant (literal, /// red and blue) symbol costs and compute the histogram aggregate bitCost. /// - private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, ushort[] binMap) + private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span binMap) { int histoSize = histograms.Count; DominantCostRange costRange = new(); @@ -198,9 +198,9 @@ internal static class HistogramEncoder private static void HistogramCombineEntropyBin( Vp8LHistogramSet histograms, Span clusters, - ushort[] clusterMappings, + Span clusterMappings, Vp8LHistogram curCombo, - ushort[] binMap, + ReadOnlySpan binMap, int numBins, double combineCostFactor) { @@ -276,7 +276,7 @@ internal static class HistogramEncoder /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values. /// - private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span symbols) + private static void OptimizeHistogramSymbols(Span clusterMappings, int numClusters, Span clusterMappingsTmp, Span symbols) { bool doContinue = true; @@ -303,7 +303,7 @@ internal static class HistogramEncoder // Create a mapping from a cluster id to its minimal version. int clusterMax = 0; - clusterMappingsTmp.AsSpan().Clear(); + clusterMappingsTmp.Clear(); // Re-map the ids. for (int i = 0; i < symbols.Length; i++) @@ -515,7 +515,6 @@ internal static class HistogramEncoder histograms.DisposeAt(idx2); // Remove pairs intersecting the just combined best pair. - // TODO: Reversing this will avoid the need to remove from the end of the list. for (int i = 0; i < histoPriorityList.Count;) { HistogramPair p = histoPriorityList[i]; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 10fc8ab804..d570bd448a 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -885,7 +885,7 @@ internal class Vp8LEncoder : IDisposable private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) { - // TODO: Allocations. + // TODO: Allocations. This method is called in a loop. int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; @@ -1628,6 +1628,7 @@ internal class Vp8LEncoder : IDisposable } } + // TODO: Allocations. int end = 5 * histogramImage.Count; for (int i = 0; i < end; i++) { @@ -1641,9 +1642,9 @@ internal class Vp8LEncoder : IDisposable } // Create Huffman trees. - // TODO: Allocations. Size here has a max and can be sliced. + // TODO: Allocations. bool[] bufRle = new bool[maxNumSymbols]; - Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols]; + HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols]; for (int i = 0; i < histogramImage.Count; i++) { From bea65987a067c0f1f9efa8bc5953d86db2a21976 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 9 Oct 2023 00:35:51 +0200 Subject: [PATCH 12/44] use pinned buffers in Vp8LHistogram --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 4 +- .../Formats/Webp/Lossless/CostModel.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 92 +++++++++++-------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 24 ++--- .../Formats/WebP/DominantCostRangeTests.cs | 23 ++--- .../Formats/WebP/Vp8LHistogramTests.cs | 6 +- 7 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index a56fc0faca..185ba1e346 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -85,7 +85,7 @@ internal static class BackwardReferenceEncoder } // Keep the best backward references. - using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBitsTmp); + using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) @@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBits); + using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 975fd581d7..94d60b4ee3 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -37,7 +37,7 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - using Vp8LHistogram histogram = new(this.memoryAllocator, cacheBits); + using Vp8LHistogram histogram = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. for (int i = 0; i < backwardRefs.Refs.Count; i++) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d570bd448a..532e75359d 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,7 +589,7 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - using Vp8LHistogram tmpHisto = new(this.memoryAllocator, cacheBits); + using Vp8LHistogram tmpHisto = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 07ee88f259..023f1c943b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -10,10 +10,20 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed class Vp8LHistogram : IDisposable +internal sealed unsafe class Vp8LHistogram : IDisposable { private const uint NonTrivialSym = 0xffffffff; - private readonly IMemoryOwner buffer; + private readonly IMemoryOwner? bufferOwner; + private readonly Memory buffer; + private readonly MemoryHandle bufferHandle; + + private readonly uint* red; + private readonly uint* blue; + private readonly uint* alpha; + private readonly uint* distance; + private readonly uint* literal; + private readonly uint* isUsed; + private const int RedSize = WebpConstants.NumLiteralCodes; private const int BlueSize = WebpConstants.NumLiteralCodes; private const int AlphaSize = WebpConstants.NumLiteralCodes; @@ -21,27 +31,6 @@ internal sealed class Vp8LHistogram : IDisposable public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1; private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize; - private readonly bool isSetMember; - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - /// The backward references to initialize the histogram with. - /// The palette code bits. - public Vp8LHistogram(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) - : this(memoryAllocator, paletteCodeBits) => this.StoreRefs(refs); - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - /// The palette code bits. - public Vp8LHistogram(MemoryAllocator memoryAllocator, int paletteCodeBits) - { - this.buffer = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); - this.PaletteCodeBits = paletteCodeBits; - } /// /// Initializes a new instance of the class. @@ -52,7 +41,7 @@ internal sealed class Vp8LHistogram : IDisposable /// The backing buffer. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(IMemoryOwner buffer, Vp8LBackwardRefs refs, int paletteCodeBits) + public Vp8LHistogram(Memory buffer, Vp8LBackwardRefs refs, int paletteCodeBits) : this(buffer, paletteCodeBits) => this.StoreRefs(refs); /// @@ -63,11 +52,20 @@ internal sealed class Vp8LHistogram : IDisposable /// /// The backing buffer. /// The palette code bits. - public Vp8LHistogram(IMemoryOwner buffer, int paletteCodeBits) + /// Optional buffer owner to dispose. + public Vp8LHistogram(Memory buffer, int paletteCodeBits, IMemoryOwner? bufferOwner = null) { + this.bufferOwner = bufferOwner; this.buffer = buffer; + this.bufferHandle = this.buffer.Pin(); this.PaletteCodeBits = paletteCodeBits; - this.isSetMember = true; + + this.red = (uint*)this.bufferHandle.Pointer; + this.blue = this.red + RedSize; + this.alpha = this.blue + BlueSize; + this.distance = this.alpha + AlphaSize; + this.literal = this.distance + DistanceSize; + this.isUsed = this.literal + LiteralSize; } /// @@ -95,22 +93,43 @@ internal sealed class Vp8LHistogram : IDisposable /// public double BlueCost { get; set; } - public Span Red => this.buffer.GetSpan()[..RedSize]; + public Span Red => new(this.red, RedSize); - public Span Blue => this.buffer.GetSpan().Slice(RedSize, BlueSize); + public Span Blue => new(this.blue, BlueSize); - public Span Alpha => this.buffer.GetSpan().Slice(RedSize + BlueSize, AlphaSize); + public Span Alpha => new(this.alpha, AlphaSize); - public Span Distance => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize, DistanceSize); + public Span Distance => new(this.distance, DistanceSize); - public Span Literal => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize, LiteralSize); + public Span Literal => new(this.literal, LiteralSize); public uint TrivialSymbol { get; set; } - private Span IsUsedSpan => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize, UsedSize); + private Span IsUsedSpan => new(this.isUsed, UsedSize); + + private Span TotalSpan => new(this.red, BufferSize); public bool IsDisposed { get; set; } + /// + /// Creates an that is not a member of a . + /// + public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) + { + IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + return new Vp8LHistogram(bufferOwner.Memory, paletteCodeBits, bufferOwner); + } + + /// + /// Creates an that is not a member of a . + /// + public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + { + Vp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); + histogram.StoreRefs(refs); + return histogram; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; @@ -140,7 +159,7 @@ internal sealed class Vp8LHistogram : IDisposable public void Clear() { - this.buffer.Clear(); + this.TotalSpan.Clear(); this.PaletteCodeBits = 0; this.BitCost = 0; this.LiteralCost = 0; @@ -607,11 +626,8 @@ internal sealed class Vp8LHistogram : IDisposable { if (!this.IsDisposed) { - if (!this.isSetMember) - { - this.buffer.Dispose(); - } - + this.bufferHandle.Dispose(); + this.bufferOwner?.Dispose(); this.IsDisposed = true; } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs index 0044c7376e..b7b884dfc8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -23,8 +23,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable this.items = new List(capacity); for (int i = 0; i < capacity; i++) { - SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); - this.items.Add(new Vp8LHistogram(owner, cacheBits)); + Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); + this.items.Add(new Vp8LHistogram(subBuffer, cacheBits)); } } @@ -35,8 +35,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable this.items = new List(capacity); for (int i = 0; i < capacity; i++) { - SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); - this.items.Add(new Vp8LHistogram(owner, refs, cacheBits)); + Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); + this.items.Add(new Vp8LHistogram(subBuffer, refs, cacheBits)); } } @@ -82,13 +82,13 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable return; } - this.buffer.Dispose(); - foreach (Vp8LHistogram item in this.items) { + // First, make sure to unpin individual sub buffers. item?.Dispose(); } + this.buffer.Dispose(); this.items.Clear(); this.isDisposed = true; } @@ -107,16 +107,4 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable } private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); - - private sealed class SetItemMemoryOwner : IMemoryOwner - { - public SetItemMemoryOwner(Memory memory) => this.Memory = memory; - - public Memory Memory { get; } - - public void Dispose() - { - // Do nothing, the underlying memory is owned by the parent set. - } - } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 80b41c5e4e..5e3f6d0c9f 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -25,12 +25,10 @@ public class DominantCostRangeTests { // arrange DominantCostRange dominantCostRange = new(); - using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 10) - { - LiteralCost = 1.0d, - RedCost = 2.0d, - BlueCost = 3.0d - }; + using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); + histogram.LiteralCost = 1.0d; + histogram.RedCost = 2.0d; + histogram.BlueCost = 3.0d; // act dominantCostRange.UpdateDominantCostRange(histogram); @@ -59,13 +57,12 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 6) - { - LiteralCost = 247.0d, - RedCost = 112.0d, - BlueCost = 202.0d, - BitCost = 733.0d - }; + using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); + histogram.LiteralCost = 247.0d; + histogram.RedCost = 112.0d; + histogram.BlueCost = 202.0d; + histogram.BitCost = 733.0d; + dominantCostRange.UpdateDominantCostRange(histogram); // act diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index 755b735452..c27d30eeab 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -78,15 +78,15 @@ public class Vp8LHistogramTests } MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; - using Vp8LHistogram histogram0 = new(memoryAllocator, backwardRefs, 3); - using Vp8LHistogram histogram1 = new(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram0 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram1 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { histogram0.IsUsed(i, true); histogram1.IsUsed(i, true); } - using Vp8LHistogram output = new(memoryAllocator, 3); + using Vp8LHistogram output = Vp8LHistogram.Create(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 83ced124c4a077eea9d913de7b6b2c74b62be182 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 16 Oct 2023 15:38:58 +1000 Subject: [PATCH 13/44] Split Vp8LHistogram and clean up --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 4 +- .../Formats/Webp/Lossless/CostModel.cs | 2 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 10 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 99 ++++++++++--------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 62 ++++++------ .../Formats/WebP/DominantCostRangeTests.cs | 4 +- .../Formats/WebP/Vp8LHistogramTests.cs | 6 +- 8 files changed, 98 insertions(+), 91 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 185ba1e346..211185dbba 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -85,7 +85,7 @@ internal static class BackwardReferenceEncoder } // Keep the best backward references. - using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) @@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBits); + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 94d60b4ee3..beebc48abc 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -37,7 +37,7 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - using Vp8LHistogram histogram = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. for (int i = 0; i < backwardRefs.Refs.Count; i++) diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index cce0356176..6c2d18a919 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -172,8 +172,8 @@ internal static class HistogramEncoder // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4)) { - origHistograms.DisposeAt(i); - histograms.DisposeAt(i); + origHistograms[i] = null; + histograms[i] = null; histogramSymbols[i] = InvalidHistogramSymbol; } else @@ -254,7 +254,7 @@ internal static class HistogramEncoder // Move the (better) merged histogram to its final slot. (histograms[first], curCombo) = (curCombo, histograms[first]); - histograms.DisposeAt(idx); + histograms[idx] = null; indicesToRemove.Add(idx); clusterMappings[clusters[idx]] = clusters[first]; } @@ -415,7 +415,7 @@ internal static class HistogramEncoder // Merge the histograms and remove bestIdx2 from the list. HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo; - histograms.DisposeAt(bestIdx2); + histograms[bestIdx2] = null; numUsed--; for (int j = 0; j < histoPriorityList.Count;) @@ -512,7 +512,7 @@ internal static class HistogramEncoder histograms[idx1].BitCost = histoPriorityList[0].CostCombo; // Remove merged histogram. - histograms.DisposeAt(idx2); + histograms[idx2] = null; // Remove pairs intersecting the just combined best pair. for (int i = 0; i < histoPriorityList.Count;) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index b53180a4ff..878d487a86 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,7 +589,7 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - using Vp8LHistogram tmpHisto = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); + using OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 023f1c943b..f473977908 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -10,13 +10,9 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed unsafe class Vp8LHistogram : IDisposable +internal abstract unsafe class Vp8LHistogram { private const uint NonTrivialSym = 0xffffffff; - private readonly IMemoryOwner? bufferOwner; - private readonly Memory buffer; - private readonly MemoryHandle bufferHandle; - private readonly uint* red; private readonly uint* blue; private readonly uint* alpha; @@ -35,32 +31,21 @@ internal sealed unsafe class Vp8LHistogram : IDisposable /// /// Initializes a new instance of the class. /// - /// - /// This constructor should be used when the histogram is a member of a . - /// - /// The backing buffer. + /// The base pointer to the backing memory. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(Memory buffer, Vp8LBackwardRefs refs, int paletteCodeBits) - : this(buffer, paletteCodeBits) => this.StoreRefs(refs); + protected Vp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(basePointer, paletteCodeBits) => this.StoreRefs(refs); /// /// Initializes a new instance of the class. /// - /// - /// This constructor should be used when the histogram is a member of a . - /// - /// The backing buffer. + /// The base pointer to the backing memory. /// The palette code bits. - /// Optional buffer owner to dispose. - public Vp8LHistogram(Memory buffer, int paletteCodeBits, IMemoryOwner? bufferOwner = null) + protected Vp8LHistogram(uint* basePointer, int paletteCodeBits) { - this.bufferOwner = bufferOwner; - this.buffer = buffer; - this.bufferHandle = this.buffer.Pin(); this.PaletteCodeBits = paletteCodeBits; - - this.red = (uint*)this.bufferHandle.Pointer; + this.red = basePointer; this.blue = this.red + RedSize; this.alpha = this.blue + BlueSize; this.distance = this.alpha + AlphaSize; @@ -109,27 +94,6 @@ internal sealed unsafe class Vp8LHistogram : IDisposable private Span TotalSpan => new(this.red, BufferSize); - public bool IsDisposed { get; set; } - - /// - /// Creates an that is not a member of a . - /// - public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) - { - IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); - return new Vp8LHistogram(bufferOwner.Memory, paletteCodeBits, bufferOwner); - } - - /// - /// Creates an that is not a member of a . - /// - public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) - { - Vp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); - histogram.StoreRefs(refs); - return histogram; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; @@ -621,14 +585,57 @@ internal sealed unsafe class Vp8LHistogram : IDisposable } } } +} + +internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable +{ + private readonly IMemoryOwner bufferOwner; + private MemoryHandle bufferHandle; + private bool isDisposed; + + private OwnedVp8LHistogram( + IMemoryOwner bufferOwner, + ref MemoryHandle bufferHandle, + uint* basePointer, + int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + this.bufferOwner = bufferOwner; + this.bufferHandle = bufferHandle; + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) + { + IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + MemoryHandle bufferHandle = bufferOwner.Memory.Pin(); + return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits); + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The backward references to initialize the histogram with. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + { + OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); + histogram.StoreRefs(refs); + return histogram; + } public void Dispose() { - if (!this.IsDisposed) + if (!this.isDisposed) { this.bufferHandle.Dispose(); - this.bufferOwner?.Dispose(); - this.IsDisposed = true; + this.bufferOwner.Dispose(); + this.isDisposed = true; } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs index b7b884dfc8..a46838ee67 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -13,30 +13,39 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable { private readonly IMemoryOwner buffer; + private MemoryHandle bufferHandle; private readonly List items; private bool isDisposed; public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits) { this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); - this.items = new List(capacity); - for (int i = 0; i < capacity; i++) + unsafe { - Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); - this.items.Add(new Vp8LHistogram(subBuffer, cacheBits)); + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits)); + } } } public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits) { this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); - this.items = new List(capacity); - for (int i = 0; i < capacity; i++) + unsafe { - Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); - this.items.Add(new Vp8LHistogram(subBuffer, refs, cacheBits)); + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits)); + } } } @@ -49,30 +58,13 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable public Vp8LHistogram this[int index] { get => this.items[index]; - - // TODO: Should we check and throw for null? set => this.items[index] = value; } - public void DisposeAt(int index) - { - this.CheckDisposed(); - - Vp8LHistogram item = this.items[index]; - item?.Dispose(); - this.items[index] = null; - } - public void RemoveAt(int index) { this.CheckDisposed(); - - Vp8LHistogram item = this.items[index]; - item?.Dispose(); this.items.RemoveAt(index); -#pragma warning disable IDE0059 // Unnecessary assignment of a value - item = null; -#pragma warning restore IDE0059 // Unnecessary assignment of a value } public void Dispose() @@ -82,13 +74,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable return; } - foreach (Vp8LHistogram item in this.items) - { - // First, make sure to unpin individual sub buffers. - item?.Dispose(); - } - this.buffer.Dispose(); + this.bufferHandle.Dispose(); this.items.Clear(); this.isDisposed = true; } @@ -107,4 +94,17 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable } private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); + + private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram + { + public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + } + + public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : base(basePointer, refs, paletteCodeBits) + { + } + } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 5e3f6d0c9f..9c48e61823 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -25,7 +25,7 @@ public class DominantCostRangeTests { // arrange DominantCostRange dominantCostRange = new(); - using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); histogram.LiteralCost = 1.0d; histogram.RedCost = 2.0d; histogram.BlueCost = 3.0d; @@ -57,7 +57,7 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); histogram.LiteralCost = 247.0d; histogram.RedCost = 112.0d; histogram.BlueCost = 202.0d; diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index c27d30eeab..cfe79e49e6 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -78,15 +78,15 @@ public class Vp8LHistogramTests } MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; - using Vp8LHistogram histogram0 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); - using Vp8LHistogram histogram1 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { histogram0.IsUsed(i, true); histogram1.IsUsed(i, true); } - using Vp8LHistogram output = Vp8LHistogram.Create(memoryAllocator, 3); + using OwnedVp8LHistogram output = OwnedVp8LHistogram.Create(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 2f61d94e37c9f7a954c0f48c4bb47ba0db80f419 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 10:25:50 +1000 Subject: [PATCH 14/44] Update src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs Co-authored-by: Anton Firszov --- src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 6c2d18a919..3a96362cfd 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -383,8 +383,7 @@ internal static class HistogramEncoder idx2 = mappings[idx2]; // Calculate cost reduction on combination. - double currCost = 0; - currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); + double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); // Found a better pair? if (currCost < 0) From aada974cdb883a84fe69eb52e98a496c06bdd0cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 22:13:05 +1000 Subject: [PATCH 15/44] 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 @@ internal readonly struct 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 @@ internal readonly struct FrameControl /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (currentRow < height) + while (currentRow < endRow) { cancellationToken.ThrowIfCancellationRequested(); while (currentRowBytesRead < bytesPerInterlaceScanline) @@ -894,7 +893,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.TransparentColor); @@ -904,7 +903,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)this.bytesPerPixel, @@ -914,7 +913,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.ColorTable); @@ -923,6 +922,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( + this.configuration, this.header.BitDepth, frameControl, scanlineSpan, @@ -935,8 +935,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -984,7 +985,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -996,7 +997,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1008,7 +1009,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1019,8 +1020,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1033,8 +1035,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; + this.quantizer = encoder.Quantizer; } /// @@ -140,63 +146,81 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } Span alpha = this.chunkDataBuffer.Span; - switch (pngMetadata.ColorType) + if (pngMetadata.ColorType == PngColorType.Rgb) { if (this.use16Bit) { @@ -957,11 +979,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable if (isFrame) { - byte[] chunkBuffer = new byte[MaxBlockSize]; - 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); + uint sequenceNumber = (uint)(frameControl.SequenceNumber + i); + this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length); } else { @@ -1075,8 +1106,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void EncodePixels(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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void EncodeAdam7IndexedPixels(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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 class PngFrameMetadata : IDeepCloneable /// 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 @@ public class PngFrameMetadata : IDeepCloneable /// 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor rowSpan, 0, 1, - hasTrans, - luminance16Trans, - luminanceTrans); + transparentColor); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -56,7 +52,7 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -184,8 +178,7 @@ internal static class PngScanlineProcessor rowSpan, 0, 1, - palette, - paletteAlpha); + palette); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -193,8 +186,7 @@ internal static class PngScanlineProcessor Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { if (palette is null) @@ -202,53 +194,31 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 1, bytesPerPixel, bytesPerSample, - hasTrans, - rgb48Trans, - rgb24Trans); + transparentColor); public static void ProcessInterlacedRgbScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -274,36 +243,18 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( + configuration, bitDepth, frameControl, scanlineSpan, @@ -403,6 +345,7 @@ internal static class PngScanlineProcessor bytesPerSample); public static void ProcessInterlacedRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -421,7 +364,7 @@ internal static class PngScanlineProcessor { 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 @@ internal static class PngScanlineProcessor } 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 16/44] 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable if (isFrame) { - uint sequenceNumber = (uint)(frameControl.SequenceNumber + i); + // We increment the sequence number for each frame chunk. + // '1' is added to the sequence number to account for the preceding frame control chunk. + uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i); this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length); } else @@ -1079,7 +1084,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } } - return numChunks; + return (uint)numChunks; } /// 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 partial class PngDecoderTests 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 partial class PngEncoderTests 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 17/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( - this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, @@ -1035,7 +1034,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor } public static void ProcessRgbaScanline( - Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -334,7 +331,6 @@ internal static class PngScanlineProcessor int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( - configuration, bitDepth, frameControl, scanlineSpan, @@ -345,7 +341,6 @@ internal static class PngScanlineProcessor bytesPerSample); public static void ProcessInterlacedRgbaScanline( - Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -356,7 +351,7 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 18/44] 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 6f52a0d13c38bceecb9187534996cf21b83f1483 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 21 Oct 2023 10:40:50 +0800 Subject: [PATCH 19/44] Preparation --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 42 ++++----- .../Formats/Webp/AnimationFrameData.cs | 43 +++++++++ .../Formats/Webp/BitWriter/BitWriterBase.cs | 93 +++++++++++++------ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 11 +-- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 11 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 80 ++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/Lossy/YuvConversion.cs | 6 +- .../Formats/Webp/WebpAnimationDecoder.cs | 42 +-------- .../Formats/Webp/WebpAnimationEncoder.cs | 12 +++ .../Formats/Webp/WebpChunkParsingUtils.cs | 52 +++++++---- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 9 ++ src/ImageSharp/Formats/Webp/WebpConstants.cs | 33 ------- .../Formats/Webp/WebpDecoderCore.cs | 5 - .../Formats/WebP/YuvConversionTests.cs | 4 +- 15 files changed, 248 insertions(+), 199 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 596715b205..a18d44fde4 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -19,7 +19,7 @@ internal static class AlphaEncoder /// Data is either compressed as lossless webp image or uncompressed. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// The global configuration. /// The memory manager. /// Whether to skip metadata encoding. @@ -27,7 +27,7 @@ internal static class AlphaEncoder /// The size in bytes of the alpha data. /// The encoded alpha data. public static IMemoryOwner EncodeAlpha( - Image image, + ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, @@ -35,9 +35,9 @@ internal static class AlphaEncoder out int size) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - IMemoryOwner alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); + int width = frame.Width; + int height = frame.Height; + IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator); if (compress) { @@ -58,9 +58,9 @@ internal static class AlphaEncoder // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, // that can improve compression. - using Image alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan()); + using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan()); - size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, alphaData); + size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData); return alphaData; } @@ -73,45 +73,45 @@ internal static class AlphaEncoder /// Store the transparency in the green channel. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - /// The transparency image. - private static Image DispatchAlphaToGreen(Image image, Span alphaData) + /// The transparency frame. + private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - Image alphaAsImage = new(width, height); + int width = frame.Width; + int height = frame.Height; + ImageFrame alphaAsFrame = new(Configuration.Default, width, height); for (int y = 0; y < height; y++) { - Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); + Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); Span pixelRow = rowBuffer.Span; Span alphaRow = alphaData.Slice(y * width, width); for (int x = 0; x < width; x++) { // Leave A/R/B channels zero'd. - pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); + pixelRow[x] = new(0, alphaRow[x], 0, 0); } } - return alphaAsImage; + return alphaAsFrame; } /// /// Extract the alpha data of the image. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// The global configuration. /// The memory manager. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - private static IMemoryOwner ExtractAlphaChannel(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; - int height = image.Height; - int width = image.Width; + Buffer2D imageBuffer = frame.PixelBuffer; + int height = frame.Height; + int width = frame.Width; IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); Span alphaData = alphaDataBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs index 714ec428ec..3400fef17d 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; + namespace SixLabors.ImageSharp.Formats.Webp; internal struct AnimationFrameData @@ -10,6 +12,11 @@ internal struct AnimationFrameData /// public uint DataSize; + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + /// /// The X coordinate of the upper left corner of the frame is Frame X * 2. /// @@ -45,4 +52,40 @@ internal struct AnimationFrameData /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// public AnimationDisposalMethod DisposalMethod; + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static AnimationFrameData Parse(BufferedReadStream stream) + { + Span buffer = stackalloc byte[4]; + + AnimationFrameData data = new() + { + DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + + // 3 bytes for the X coordinate of the upper left corner of the frame. + X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // 3 bytes for the Y coordinate of the upper left corner of the frame. + Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // Frame width Minus One. + Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame height Minus One. + Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame duration. + Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + }; + + byte flags = (byte)stream.ReadByte(); + data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + + return data; + } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ab78d18604..d7787b3a00 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -92,7 +93,7 @@ internal abstract class BitWriterBase { stream.Write(WebpConstants.RiffFourCc); BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span.Slice(0, 4)); + stream.Write(this.scratchBuffer.Span[..4]); stream.Write(WebpConstants.WebpHeader); } @@ -129,7 +130,7 @@ internal abstract class BitWriterBase DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -143,6 +144,61 @@ internal abstract class BitWriterBase } } + /// + /// Writes the color profile() to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + + /// + /// Writes the animation parameter() to the stream. + /// + /// The stream to write to. + /// + /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + /// The number of times to loop the animation. If it is 0, this means infinitely. + protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + { + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + stream.Write(buf); + BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); + stream.Write(buf[..2]); + } + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + /// Animation frame data. + /// Frame data. + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + { + uint size = AnimationFrameData.HeaderSize + (uint)data.Length; + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); + stream.Write(buf); + BinaryPrimitives.WriteUInt32BigEndian(buf, size); + stream.Write(buf); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); + stream.WriteByte(flag); + stream.Write(data); + } + /// /// Writes the alpha chunk to the stream. /// @@ -152,7 +208,7 @@ internal abstract class BitWriterBase protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -161,7 +217,7 @@ internal abstract class BitWriterBase byte flags = 0; if (alphaDataIsCompressed) { - flags |= 1; + flags = 1; } stream.WriteByte(flags); @@ -174,30 +230,6 @@ internal abstract class BitWriterBase } } - /// - /// Writes the color profile to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) - { - uint size = (uint)iccProfileBytes.Length; - - Span buf = this.scratchBuffer.Span.Slice(0, 4); - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - - stream.Write(iccProfileBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } - } - /// /// Writes a VP8X header to the stream. /// @@ -246,8 +278,9 @@ internal abstract class BitWriterBase flags |= 32; } - Span buf = this.scratchBuffer.Span.Slice(0, 4); - stream.Write(WebpConstants.Vp8XMagicBytes); + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); + stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5b4eab64a3..597ecef42a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -701,12 +701,11 @@ internal class Vp8BitWriter : BitWriterBase private void WriteVp8Header(Stream stream, uint size) { - Span vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize]; - - WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); - BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size); - - stream.Write(vp8ChunkHeader); + Span buf = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); } private void WriteFrameHeader(Stream stream, uint size0) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 9dc7912392..a042f68968 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -105,7 +105,7 @@ internal class Vp8LBitWriter : BitWriterBase { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); + return new(clonedBuffer, this.bits, this.used, this.cur); } /// @@ -186,12 +186,13 @@ internal class Vp8LBitWriter : BitWriterBase } // Write magic bytes indicating its a lossless webp. - stream.Write(WebpConstants.Vp8LMagicBytes); + Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L); + stream.Write(scratchBuffer); // Write Vp8 Header. - Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); - stream.Write(scratchBuffer.Slice(0, 4)); + stream.Write(scratchBuffer); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); // Write the encoded bytes of the image to the stream. @@ -226,7 +227,7 @@ internal class Vp8LBitWriter : BitWriterBase Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); - scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); + scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur)); this.cur += WriterBytes; this.bits >>= WriterBits; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 469e4c9ab0..9b82cc5983 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -254,7 +254,7 @@ internal class Vp8LEncoder : IDisposable XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(image.Frames.RootFrame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -263,7 +263,7 @@ internal class Vp8LEncoder : IDisposable this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image); + this.EncodeStream(image.Frames.RootFrame); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); @@ -273,23 +273,23 @@ internal class Vp8LEncoder : IDisposable /// Encodes the alpha image data using the webp lossless compression. /// /// The type of the pixel. - /// The to encode from. + /// The to encode from. /// The destination buffer to write the encoded alpha data to. /// The size of the compressed data in bytes. /// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed. /// - public int EncodeAlphaImageData(Image image, IMemoryOwner alphaData) + public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; int pixelCount = width * height; // Convert image pixels to bgra array. - this.ConvertPixelsToBgra(image, width, height); + this.ConvertPixelsToBgra(frame, width, height); // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. - this.EncodeStream(image); + this.EncodeStream(frame); this.bitWriter.Finish(); int size = this.bitWriter.NumBytes(); if (size >= pixelCount) @@ -333,12 +333,12 @@ internal class Vp8LEncoder : IDisposable /// Encodes the image stream using lossless webp format. /// /// The pixel type. - /// The image to encode. - private void EncodeStream(Image image) + /// The frame to encode. + private void EncodeStream(ImageFrame frame) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); @@ -447,14 +447,14 @@ internal class Vp8LEncoder : IDisposable /// Converts the pixels of the image to bgra. /// /// The type of the pixels. - /// The image to convert. + /// The frame to convert. /// The width of the image. /// The height of the image. /// true, if the image is non opaque. - private bool ConvertPixelsToBgra(Image image, int width, int height) + private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; bool nonOpaque = false; Span bgra = this.Bgra.GetSpan(); Span bgraBytes = MemoryMarshal.Cast(bgra); @@ -1149,35 +1149,41 @@ internal class Vp8LEncoder : IDisposable entropyComp[j] = bitEntropy.BitsEntropyRefine(); } - entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRed] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlue]; - entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPred] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePred]; - entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRedSubGreen] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlueSubGreen]; - entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPredSubGreen] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePredSubGreen]; + entropy[(int)EntropyIx.Direct] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRed] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlue]; + entropy[(int)EntropyIx.Spatial] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPred] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePred]; + entropy[(int)EntropyIx.SubGreen] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRedSubGreen] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlueSubGreen]; + entropy[(int)EntropyIx.SpatialSubGreen] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPredSubGreen] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePredSubGreen]; entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette]; // When including transforms, there is an overhead in bits from // storing them. This overhead is small but matters for small images. // For spatial, there are 14 transformations. - entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(14); + entropy[(int)EntropyIx.Spatial] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(14); // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. - entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(24); + entropy[(int)EntropyIx.SpatialSubGreen] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(24); // For palettes, add the cost of storing the palette. // We empirically estimate the cost of a compressed entry as 8 bits. diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f17d965e87..ce5d3bac11 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -323,7 +323,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -393,7 +393,7 @@ internal class Vp8Encoder : IDisposable { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image, + image.Frames.RootFrame, this.configuration, this.memoryAllocator, this.skipMetadata, diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 8ef7fe9cba..d669a37b74 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -262,17 +262,17 @@ internal static class YuvConversion /// Converts the RGB values of the image to YUV. /// /// The pixel type of the image. - /// The image to convert. + /// The frame to convert. /// The global configuration. /// The memory allocator. /// Span to store the luma component of the image. /// Span to store the u component of the image. /// Span to store the v component of the image. /// true, if the image contains alpha data. - public static bool ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; int width = imageBuffer.Width; int height = imageBuffer.Height; int uvWidth = (width + 1) >> 1; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 90c9c70b26..65f654dddc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = this.ReadFrameHeader(stream); + AnimationFrameData frameData = AnimationFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -173,7 +173,7 @@ internal class WebpAnimationDecoder : IDisposable ImageFrame imageFrame; if (previousFrame is null) { - image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -258,7 +258,7 @@ internal class WebpAnimationDecoder : IDisposable try { - Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; + Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -353,42 +353,6 @@ internal class WebpAnimationDecoder : IDisposable pixelRegion.Fill(backgroundPixel); } - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - AnimationFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; - - return data; - } - /// public void Dispose() => this.alphaData?.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs new file mode 100644 index 0000000000..bfa64b6797 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Encoder for animated webp images. +/// +public class WebpAnimationEncoder +{ + // 可能不需要这个屌东西 +} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index a7ae474e46..becd622e17 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,13 +2,12 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -91,7 +90,7 @@ internal static class WebpChunkParsingUtils uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); - tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]); uint height = tmp & 0x3fff; sbyte yScale = (sbyte)(tmp >> 6); remaining -= 7; @@ -105,14 +104,14 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - var vp8FrameHeader = new Vp8FrameHeader() + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - var bitReader = new Vp8BitReader( + Vp8BitReader bitReader = new( stream, remaining, memoryAllocator, @@ -121,7 +120,7 @@ internal static class WebpChunkParsingUtils Remaining = remaining }; - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -145,7 +144,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -174,7 +173,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -231,13 +230,13 @@ internal static class WebpChunkParsingUtils } // 3 bytes for the width. - uint width = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint width = ReadUInt24LittleEndian(stream, buffer) + 1; // 3 bytes for the height. - uint height = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - var info = new WebpImageInfo() + WebpImageInfo info = new() { Width = width, Height = height, @@ -253,7 +252,7 @@ internal static class WebpChunkParsingUtils /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -261,7 +260,28 @@ internal static class WebpChunkParsingUtils return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } - throw new ImageFormatException("Invalid Webp data, could not read unsigned integer."); + throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer."); + } + + /// + /// Writes a unsigned 24 bit integer. + /// + /// The stream to read from. + /// The uint24 data to write. + public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) + { + if (data >= 1 << 24) + { + throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer."); + } + + uint* ptr = &data; + byte* b = (byte*)ptr; + + // Write the data in little endian. + stream.WriteByte(b[0]); + stream.WriteByte(b[1]); + stream.WriteByte(b[2]); } /// @@ -298,7 +318,7 @@ internal static class WebpChunkParsingUtils if (stream.Read(buffer) == 4) { - var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); return chunkType; } @@ -335,7 +355,7 @@ internal static class WebpChunkParsingUtils if (metadata.ExifProfile != null) { - metadata.ExifProfile = new ExifProfile(exifData); + metadata.ExifProfile = new(exifData); } break; @@ -349,7 +369,7 @@ internal static class WebpChunkParsingUtils if (metadata.XmpProfile != null) { - metadata.XmpProfile = new XmpProfile(xmpData); + metadata.XmpProfile = new(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 802d7f7288..5836dc6c09 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -12,45 +12,54 @@ internal enum WebpChunkType : uint /// /// Header signaling the use of the VP8 format. /// + /// VP8 (Single) Vp8 = 0x56503820U, /// /// Header signaling the image uses lossless encoding. /// + /// VP8L (Single) Vp8L = 0x5650384CU, /// /// Header for a extended-VP8 chunk. /// + /// VP8X (Single) Vp8X = 0x56503858U, /// /// Chunk contains information about the alpha channel. /// + /// ALPH (Single) Alpha = 0x414C5048U, /// /// Chunk which contains a color profile. /// + /// ICCP (Single) Iccp = 0x49434350U, /// /// Chunk which contains EXIF metadata about the image. /// + /// EXIF (Single) Exif = 0x45584946U, /// /// Chunk contains XMP metadata about the image. /// + /// XMP (Single) Xmp = 0x584D5020U, /// /// For an animated image, this chunk contains the global parameters of the animation. /// + /// ANIM (Single) AnimationParameter = 0x414E494D, /// /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// + /// ANMF (Multiple) Animation = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index d105d8dd62..1433772757 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -33,39 +33,6 @@ internal static class WebpConstants /// public const byte Vp8LHeaderMagicByte = 0x2F; - /// - /// Signature bytes identifying a lossy image. - /// - public static readonly byte[] Vp8MagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x20 // ' ' - }; - - /// - /// Signature bytes identifying a lossless image. - /// - public static readonly byte[] Vp8LMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x4C // L - }; - - /// - /// Signature bytes identifying a VP8X header. - /// - public static readonly byte[] Vp8XMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x58 // X - }; - /// /// The header bytes identifying RIFF file. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 8832ac1068..63d3e1aead 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -93,11 +93,6 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } - if (this.webImageInfo.Features is { Animation: true }) - { - WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); - } - image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 9b03a447a9..433b280bc3 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -143,7 +143,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); @@ -249,7 +249,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); From 62ab3a1eef3c9e9af4af683f512471f81bf3e5d4 Mon Sep 17 00:00:00 2001 From: Poker Date: Sun, 22 Oct 2023 22:54:33 +0800 Subject: [PATCH 20/44] refactor --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 11 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 104 +++++++++--------- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 5 + .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 5 + 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index d7787b3a00..4252f895b8 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -181,7 +180,7 @@ internal abstract class BitWriterBase /// The stream to write to. /// Animation frame data. /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) { uint size = AnimationFrameData.HeaderSize + (uint)data.Length; Span buf = this.scratchBuffer.Span[..4]; @@ -260,6 +259,14 @@ internal abstract class BitWriterBase flags |= 8; } + /* + if (isAnimated) + { + // Set animated flag. + flags |= 2; + } + */ + if (xmpProfile != null) { // Set xmp bit. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 597ecef42a..cd84f109eb 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -116,7 +116,7 @@ internal class Vp8BitWriter : BitWriterBase else { this.PutBit(v >= 9, 165); - this.PutBit(!((v & 1) != 0), 145); + this.PutBit((v & 1) == 0, 145); } } else @@ -462,7 +462,7 @@ internal class Vp8BitWriter : BitWriterBase Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); // Partition #0 with header and partition sizes. - uint size0 = this.GeneratePartition0(bitWriterPartZero); + uint size0 = bitWriterPartZero.GeneratePartition0(); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; vp8Size += numBytes; @@ -495,47 +495,47 @@ internal class Vp8BitWriter : BitWriterBase } } - private uint GeneratePartition0(Vp8BitWriter bitWriter) + private uint GeneratePartition0() { - bitWriter.PutBitUniform(0); // colorspace - bitWriter.PutBitUniform(0); // clamp type + this.PutBitUniform(0); // colorspace + this.PutBitUniform(0); // clamp type - this.WriteSegmentHeader(bitWriter); - this.WriteFilterHeader(bitWriter); + this.WriteSegmentHeader(); + this.WriteFilterHeader(); - bitWriter.PutBits(0, 2); + this.PutBits(0, 2); - this.WriteQuant(bitWriter); - bitWriter.PutBitUniform(0); - this.WriteProbas(bitWriter); - this.CodeIntraModes(bitWriter); + this.WriteQuant(); + this.PutBitUniform(0); + this.WriteProbas(); + this.CodeIntraModes(); - bitWriter.Finish(); + this.Finish(); - return (uint)bitWriter.NumBytes(); + return (uint)this.NumBytes(); } - private void WriteSegmentHeader(Vp8BitWriter bitWriter) + private void WriteSegmentHeader() { Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; Vp8EncProba proba = this.enc.Proba; - if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) { // We always 'update' the quant and filter strength values. int updateData = 1; - bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); - if (bitWriter.PutBitUniform(updateData) != 0) + this.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (this.PutBitUniform(updateData) != 0) { // We always use absolute values, not relative ones. - bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); } for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); } } @@ -543,50 +543,50 @@ internal class Vp8BitWriter : BitWriterBase { for (int s = 0; s < 3; ++s) { - if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) + if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) { - bitWriter.PutBits(proba.Segments[s], 8); + this.PutBits(proba.Segments[s], 8); } } } } } - private void WriteFilterHeader(Vp8BitWriter bitWriter) + private void WriteFilterHeader() { Vp8FilterHeader hdr = this.enc.FilterHeader; bool useLfDelta = hdr.I4x4LfDelta != 0; - bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); - bitWriter.PutBits((uint)hdr.FilterLevel, 6); - bitWriter.PutBits((uint)hdr.Sharpness, 3); - if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) + this.PutBitUniform(hdr.Simple ? 1 : 0); + this.PutBits((uint)hdr.FilterLevel, 6); + this.PutBits((uint)hdr.Sharpness, 3); + if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0) { // '0' is the default value for i4x4LfDelta at frame #0. bool needUpdate = hdr.I4x4LfDelta != 0; - if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0) + if (this.PutBitUniform(needUpdate ? 1 : 0) != 0) { // we don't use refLfDelta => emit four 0 bits. - bitWriter.PutBits(0, 4); + this.PutBits(0, 4); // we use modeLfDelta for i4x4 - bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); - bitWriter.PutBits(0, 3); // all others unused. + this.PutSignedBits(hdr.I4x4LfDelta, 6); + this.PutBits(0, 3); // all others unused. } } } // Nominal quantization parameters - private void WriteQuant(Vp8BitWriter bitWriter) + private void WriteQuant() { - bitWriter.PutBits((uint)this.enc.BaseQuant, 7); - bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); - bitWriter.PutSignedBits(this.enc.DqUvDc, 4); - bitWriter.PutSignedBits(this.enc.DqUvAc, 4); + this.PutBits((uint)this.enc.BaseQuant, 7); + this.PutSignedBits(this.enc.DqY1Dc, 4); + this.PutSignedBits(this.enc.DqY2Dc, 4); + this.PutSignedBits(this.enc.DqY2Ac, 4); + this.PutSignedBits(this.enc.DqUvDc, 4); + this.PutSignedBits(this.enc.DqUvAc, 4); } - private void WriteProbas(Vp8BitWriter bitWriter) + private void WriteProbas() { Vp8EncProba probas = this.enc.Proba; for (int t = 0; t < WebpConstants.NumTypes; ++t) @@ -599,25 +599,25 @@ internal class Vp8BitWriter : BitWriterBase { byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; - if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) + if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) { - bitWriter.PutBits(p0, 8); + this.PutBits(p0, 8); } } } } } - if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) { - bitWriter.PutBits(probas.SkipProba, 8); + this.PutBits(probas.SkipProba, 8); } } // Writes the partition #0 modes (that is: all intra modes) - private void CodeIntraModes(Vp8BitWriter bitWriter) + private void CodeIntraModes() { - var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh); + Vp8EncIterator it = new(this.enc); int predsWidth = this.enc.PredsWidth; do @@ -627,18 +627,18 @@ internal class Vp8BitWriter : BitWriterBase Span preds = it.Preds.AsSpan(predIdx); if (this.enc.SegmentHeader.UpdateMap) { - bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments); + this.PutSegment(mb.Segment, this.enc.Proba.Segments); } if (this.enc.Proba.UseSkipProba) { - bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba); + this.PutBit(mb.Skip, this.enc.Proba.SkipProba); } - if (bitWriter.PutBit(mb.MacroBlockType != 0, 145)) + if (this.PutBit(mb.MacroBlockType != 0, 145)) { // i16x16 - bitWriter.PutI16Mode(preds[0]); + this.PutI16Mode(preds[0]); } else { @@ -649,7 +649,7 @@ internal class Vp8BitWriter : BitWriterBase for (int x = 0; x < 4; x++) { byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; - left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas); + left = this.PutI4Mode(it.Preds[predIdx + x], probas); } topPred = it.Preds.AsSpan(predIdx); @@ -657,7 +657,7 @@ internal class Vp8BitWriter : BitWriterBase } } - bitWriter.PutUvMode(mb.UvMode); + this.PutUvMode(mb.UvMode); } while (it.Next()); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index 7211f93766..a7c96edb7c 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -50,6 +50,11 @@ internal class Vp8EncIterator private int uvTopIdx; + public Vp8EncIterator(Vp8Encoder enc) + : this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh) + { + } + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) { this.YTop = yTop; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ce5d3bac11..c65099af88 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -328,7 +328,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -520,7 +520,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 65f654dddc..81a7aebdf9 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -162,6 +162,11 @@ internal class WebpAnimationDecoder : IDisposable features.AlphaChunkHeader = alphaChunkHeader; break; case WebpChunkType.Vp8L: + if (hasAlpha) + { + WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images."); + } + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); break; default: From 5ed6f24943b697d2c354d762ebaaa6aecee27274 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 21:54:53 +1000 Subject: [PATCH 21/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, this.header.BitDepth, in frameControl, scanlineSpan, @@ -961,7 +968,13 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ public class PngFrameMetadata : IDeepCloneable /// 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor 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 @@ internal static class PngScanlineProcessor } 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 @@ internal static class PngScanlineProcessor 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 22/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata); - + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata); this.SwapScanlineBuffers(); currentRow++; } @@ -759,7 +785,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// Decodes the raw interlaced pixel data row by row - /// /// /// The pixel format. /// The frame control @@ -767,7 +792,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); + // TODO: Alpha blending. this.SwapScanlineBuffers(); currentRow += Adam7.RowIncrement[pass]; @@ -874,7 +905,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } 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 95d36af396d5f57a9d79ca1e3a158c9b18e95a4a Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 19:50:28 +0800 Subject: [PATCH 23/44] (Vp8) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 47 ++++-- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 135 +++++++----------- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 10 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 27 ++-- .../Formats/Webp/WebpChunkParsingUtils.cs | 11 +- .../Metadata/Profiles/ICC/IccProfile.cs | 4 +- 7 files changed, 116 insertions(+), 126 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4252f895b8..b82b764fc3 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -41,17 +42,23 @@ internal abstract class BitWriterBase public byte[] Buffer => this.buffer; + /// + /// Gets the number of bytes of the encoded image data. + /// + /// The number of bytes of the image data. + public abstract int NumBytes { get; } + /// /// Writes the encoded bytes of the image to the stream. Call Finish() before this. /// /// The stream to write to. - public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); + public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes)); /// /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. /// /// The destination buffer. - public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest); + public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest); /// /// Resizes the buffer to write to. @@ -59,12 +66,6 @@ internal abstract class BitWriterBase /// The extra size in bytes needed. public abstract void BitWriterResize(int extraSize); - /// - /// Returns the number of bytes of the encoded image data. - /// - /// The number of bytes of the image data. - public abstract int NumBytes(); - /// /// Flush leftover bits. /// @@ -86,6 +87,7 @@ internal abstract class BitWriterBase /// /// Writes the RIFF header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. protected void WriteRiffHeader(Stream stream, uint riffSize) @@ -99,6 +101,7 @@ internal abstract class BitWriterBase /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// + /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -118,9 +121,26 @@ internal abstract class BitWriterBase return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); } + /// + /// Overwrites ides the write file size. + /// + /// The stream to write to. + protected static void OverwriteFileSize(Stream stream) + { + uint position = (uint)stream.Position; + stream.Position = 4; + byte[] buffer = new byte[4]; + + // "RIFF"(4)+uint32 size(4) + BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); + stream.Write(buffer); + stream.Position = position; + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. @@ -146,6 +166,7 @@ internal abstract class BitWriterBase /// /// Writes the color profile() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); @@ -153,6 +174,7 @@ internal abstract class BitWriterBase /// /// Writes the animation parameter() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -177,6 +199,7 @@ internal abstract class BitWriterBase /// /// Writes the animation frame() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. /// Frame data. @@ -201,6 +224,7 @@ internal abstract class BitWriterBase /// /// Writes the alpha chunk to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. @@ -232,14 +256,15 @@ internal abstract class BitWriterBase /// /// Writes a VP8X header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. - /// The color profile bytes. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -279,7 +304,7 @@ internal abstract class BitWriterBase flags |= 16; } - if (iccProfileBytes != null) + if (iccProfile != null) { // Set iccp flag. flags |= 32; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index cd84f109eb..5dd5d335de 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -72,7 +72,7 @@ internal class Vp8BitWriter : BitWriterBase } /// - public override int NumBytes() => (int)this.pos; + public override int NumBytes => (int)this.pos; public int PutCoeffs(int ctx, Vp8Residual residual) { @@ -395,67 +395,58 @@ internal class Vp8BitWriter : BitWriterBase } /// - /// Writes the encoded image to the stream. + /// Write the trunks before data trunk. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. + /// The width of the image. + /// The height of the image. /// The exif profile. /// The XMP profile. /// The color profile. - /// The width of the image. - /// The height of the image. /// Flag indicating, if a alpha channel is present. /// The alpha channel data. /// Indicates, if the alpha data is compressed. - public void WriteEncodedImageToStream( + public void WriteTrunksBeforeData( Stream stream, + uint width, + uint height, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, - uint width, - uint height, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccProfileBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } + // Write file size later + this.WriteRiffHeader(stream, 0); - if (xmpProfile != null) + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - if (iccProfile != null) - { - isVp8X = true; - iccProfileBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccProfileBytes); - } + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } - if (hasAlpha) - { - isVp8X = true; - riffSize += AlphaChunkSize(alphaData); + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } } + } - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public void WriteEncodedImageToStream(Stream stream) + { + uint numBytes = (uint)this.NumBytes; - this.Finish(); - uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); @@ -469,12 +460,10 @@ internal class Vp8BitWriter : BitWriterBase uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size. - // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; + // Emit header and partition #0 + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); - // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -483,16 +472,31 @@ internal class Vp8BitWriter : BitWriterBase { stream.WriteByte(0); } + } + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } + + OverwriteFileSize(stream); } private uint GeneratePartition0() @@ -512,7 +516,7 @@ internal class Vp8BitWriter : BitWriterBase this.Finish(); - return (uint)this.NumBytes(); + return (uint)this.NumBytes; } private void WriteSegmentHeader() @@ -662,43 +666,6 @@ internal class Vp8BitWriter : BitWriterBase while (it.Next()); } - private void WriteWebpHeaders( - Stream stream, - uint size0, - uint vp8Size, - uint riffSize, - bool isVp8X, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - byte[]? iccProfileBytes, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); - - if (iccProfileBytes != null) - { - this.WriteColorProfile(stream, iccProfileBytes); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - - this.WriteVp8Header(stream, vp8Size); - this.WriteFrameHeader(stream, size0); - } - private void WriteVp8Header(Stream stream, uint size) { Span buf = stackalloc byte[WebpConstants.TagSize]; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index a042f68968..0ac1b4038a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -47,6 +47,9 @@ internal class Vp8LBitWriter : BitWriterBase { } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -98,9 +101,6 @@ internal class Vp8LBitWriter : BitWriterBase this.PutBits((uint)((bits << depth) | symbol), depth + nBits); } - /// - public override int NumBytes() => this.cur + ((this.used + 7) >> 3); - public Vp8LBitWriter Clone() { byte[] clonedBuffer = new byte[this.Buffer.Length]; @@ -166,7 +166,7 @@ internal class Vp8LBitWriter : BitWriterBase } this.Finish(); - uint size = (uint)this.NumBytes(); + uint size = (uint)this.NumBytes; size++; // One byte extra for the VP8L signature. // Write RIFF header. @@ -177,7 +177,7 @@ internal class Vp8LBitWriter : BitWriterBase // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); if (iccBytes != null) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9b82cc5983..d27bfcd956 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -291,7 +291,7 @@ internal class Vp8LEncoder : IDisposable // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. this.EncodeStream(frame); this.bitWriter.Finish(); - int size = this.bitWriter.NumBytes(); + int size = this.bitWriter.NumBytes; if (size >= pixelCount) { // Compressing would not yield in smaller data -> leave the data uncompressed. @@ -425,9 +425,9 @@ internal class Vp8LEncoder : IDisposable lowEffort); // If we are better than what we already have. - if (isFirstConfig || this.bitWriter.NumBytes() < bestSize) + if (isFirstConfig || this.bitWriter.NumBytes < bestSize) { - bestSize = this.bitWriter.NumBytes(); + bestSize = this.bitWriter.NumBytes; BitWriterSwap(ref this.bitWriter, ref bitWriterBest); } @@ -676,7 +676,7 @@ internal class Vp8LEncoder : IDisposable this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); // Keep track of the smallest image so far. - if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes)) { Vp8LBitWriter tmp = this.bitWriter; this.bitWriter = bitWriterBest; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index c65099af88..56397e66d4 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -408,16 +409,21 @@ internal class Vp8Encoder : IDisposable } } - this.bitWriter.WriteEncodedImageToStream( + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( stream, + (uint)width, + (uint)height, exifProfile, xmpProfile, metadata.IccProfile, - (uint)width, - (uint)height, hasAlpha, alphaData[..alphaDataSize], this.alphaCompression && alphaCompressionSucceeded); + + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } finally { @@ -862,10 +868,11 @@ internal class Vp8Encoder : IDisposable this.ResetSegments(); } - this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + - (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + - (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + - (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); + this.SegmentHeader.Size = + (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + + (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + + (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); } else { @@ -1027,7 +1034,7 @@ internal class Vp8Encoder : IDisposable it.NzToBytes(); - int pos1 = this.bitWriter.NumBytes(); + int pos1 = this.bitWriter.NumBytes; if (i16) { residual.Init(0, 1, this.Proba); @@ -1054,7 +1061,7 @@ internal class Vp8Encoder : IDisposable } } - int pos2 = this.bitWriter.NumBytes(); + int pos2 = this.bitWriter.NumBytes; // U/V residual.Init(0, 2, this.Proba); @@ -1072,7 +1079,7 @@ internal class Vp8Encoder : IDisposable } } - int pos3 = this.bitWriter.NumBytes(); + int pos3 = this.bitWriter.NumBytes; it.LumaBits = pos2 - pos1; it.UvBits = pos3 - pos2; it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index becd622e17..9e9f0f7f62 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -76,7 +76,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); } - if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) + if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } @@ -111,14 +111,7 @@ internal static class WebpChunkParsingUtils PartitionLength = partitionLength }; - Vp8BitReader bitReader = new( - stream, - remaining, - memoryAllocator, - partitionLength) - { - Remaining = remaining - }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new() { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 3b5e438299..be7350bc44 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -158,8 +158,7 @@ public sealed class IccProfile : IDeepCloneable Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && - this.Header.Size >= minSize && - this.Header.Size < maxSize; + this.Header.Size is >= minSize and < maxSize; } /// @@ -175,7 +174,6 @@ public sealed class IccProfile : IDeepCloneable return copy; } - IccWriter writer = new(); return IccWriter.Write(this); } From 845527587c8bd8788ac79e401527ddf71e8b33dc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 22:52:53 +1000 Subject: [PATCH 24/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 437144dab5bb08743aa43e2411ec7bd38963b1c0 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 21:14:31 +0800 Subject: [PATCH 25/44] (Vp8L) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 76 ++++++++++++++++++ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 80 +------------------ .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 76 +----------------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 17 +++- .../Formats/Webp/Lossy/Vp8Encoder.cs | 1 - 5 files changed, 97 insertions(+), 153 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index b82b764fc3..ad7d69f130 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -137,6 +137,82 @@ internal abstract class BitWriterBase stream.Position = position; } + /// + /// Write the trunks before data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The width of the image. + /// The height of the image. + /// The exif profile. + /// The XMP profile. + /// The color profile. + /// Flag indicating, if a alpha channel is present. + /// The alpha channel data. + /// Indicates, if the alpha data is compressed. + public void WriteTrunksBeforeData( + Stream stream, + uint width, + uint height, + ExifProfile? exifProfile, + XmpProfile? xmpProfile, + IccProfile? iccProfile, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) + { + // Write file size later + this.WriteRiffHeader(stream, 0); + + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } + + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } + } + } + + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); + + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { + if (exifProfile != null) + { + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + } + + OverwriteFileSize(stream); + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5dd5d335de..923d2a69c4 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossy; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -394,56 +391,8 @@ internal class Vp8BitWriter : BitWriterBase } } - /// - /// Write the trunks before data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The width of the image. - /// The height of the image. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( - Stream stream, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - IccProfile? iccProfile, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - // Write file size later - this.WriteRiffHeader(stream, 0); - - // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccProfile != null) - { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - } - - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - public void WriteEncodedImageToStream(Stream stream) + /// + public override void WriteEncodedImageToStream(Stream stream) { uint numBytes = (uint)this.NumBytes; @@ -474,31 +423,6 @@ internal class Vp8BitWriter : BitWriterBase } } - /// - /// Write the trunks after data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - public void WriteTrunksAfterData( - Stream stream, - ExifProfile? exifProfile, - XmpProfile? xmpProfile) - { - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); - } - - OverwriteFileSize(stream); - } - private uint GeneratePartition0() { this.PutBitUniform(0); // colorspace diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0ac1b4038a..bce77c9e5c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossless; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -122,68 +119,11 @@ internal class Vp8LBitWriter : BitWriterBase this.used = 0; } - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// The width of the image. - /// The height of the image. - /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// + public override void WriteEncodedImageToStream(Stream stream) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } - - if (xmpProfile != null) - { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } - - if (iccProfile != null) - { - isVp8X = true; - iccBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccBytes); - } - - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } - - this.Finish(); - uint size = (uint)this.NumBytes; - size++; // One byte extra for the VP8L signature. - - // Write RIFF header. + uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature uint pad = size & 1; - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccBytes != null) - { - this.WriteColorProfile(stream, iccBytes); - } - } // Write magic bytes indicating its a lossless webp. Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; @@ -201,16 +141,6 @@ internal class Vp8LBitWriter : BitWriterBase { stream.WriteByte(0); } - - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); - } } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d27bfcd956..4d526e7b4b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -265,8 +266,22 @@ internal class Vp8LEncoder : IDisposable // Encode the main image stream. this.EncodeStream(image.Frames.RootFrame); + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( + stream, + (uint)width, + (uint)height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false /*hasAlpha*/, + Span.Empty, + false); + // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 56397e66d4..f744827bf3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; From fbc08bd6a683a8c96777dfc17441d3fabb7f554c Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:21:11 +0800 Subject: [PATCH 26/44] Implement Vp8 encoder --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 106 +++++++-------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 5 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 121 ++++++++++++++---- .../Formats/Webp/WebpEncoderCore.cs | 30 ++++- .../Formats/WebP/WebpEncoderTests.cs | 7 + 5 files changed, 180 insertions(+), 89 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ad7d69f130..4a9da3cbb1 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -87,21 +87,20 @@ internal abstract class BitWriterBase /// /// Writes the RIFF header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. - protected void WriteRiffHeader(Stream stream, uint riffSize) + protected static void WriteRiffHeader(Stream stream, uint riffSize) { stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span[..4]); + Span buf = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); + stream.Write(buf); stream.Write(WebpConstants.WebpHeader); } /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// - /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -125,22 +124,11 @@ internal abstract class BitWriterBase /// Overwrites ides the write file size. /// /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) - { - uint position = (uint)stream.Position; - stream.Position = 4; - byte[] buffer = new byte[4]; - - // "RIFF"(4)+uint32 size(4) - BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); - stream.Write(buffer); - stream.Position = position; - } + protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); /// /// Write the trunks before data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The width of the image. /// The height of the image. @@ -148,9 +136,8 @@ internal abstract class BitWriterBase /// The XMP profile. /// The color profile. /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( + /// Flag indicating, if an animation parameter is present. + public static void WriteTrunksBeforeData( Stream stream, uint width, uint height, @@ -158,26 +145,20 @@ internal abstract class BitWriterBase XmpProfile? xmpProfile, IccProfile? iccProfile, bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) + bool hasAnimation) { // Write file size later - this.WriteRiffHeader(stream, 0); + WriteRiffHeader(stream, 0); // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); if (iccProfile != null) { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + WriteColorProfile(stream, iccProfile.ToByteArray()); } } } @@ -191,23 +172,22 @@ internal abstract class BitWriterBase /// /// Write the trunks after data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The exif profile. /// The XMP profile. - public void WriteTrunksAfterData( + public static void WriteTrunksAfterData( Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile) { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } OverwriteFileSize(stream); @@ -216,16 +196,15 @@ internal abstract class BitWriterBase /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. - protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) + protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) { DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -242,15 +221,13 @@ internal abstract class BitWriterBase /// /// Writes the color profile() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); /// /// Writes the animation parameter() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -259,9 +236,9 @@ internal abstract class BitWriterBase /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) { - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); @@ -275,17 +252,15 @@ internal abstract class BitWriterBase /// /// Writes the animation frame() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. - /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) + public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) { - uint size = AnimationFrameData.HeaderSize + (uint)data.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); stream.Write(buf); - BinaryPrimitives.WriteUInt32BigEndian(buf, size); + long position = stream.Position; + BinaryPrimitives.WriteUInt32BigEndian(buf, 0); stream.Write(buf); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); @@ -294,20 +269,35 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); - stream.Write(data); + return position; + } + + /// + /// Overwrites ides the write frame size. + /// + /// The stream to write to. + /// Previous position. + public static void OverwriteFrameSize(Stream stream, long prevPosition) + { + uint position = (uint)stream.Position; + stream.Position = prevPosition; + byte[] buffer = new byte[4]; + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); + stream.Write(buffer); + stream.Position = position; } /// /// Writes the alpha chunk to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. - protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) + public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -332,7 +322,6 @@ internal abstract class BitWriterBase /// /// Writes a VP8X header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. @@ -340,7 +329,8 @@ internal abstract class BitWriterBase /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// Flag indicating, if an animation parameter is present. + protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { if (width > MaxDimension || height > MaxDimension) { @@ -360,13 +350,11 @@ internal abstract class BitWriterBase flags |= 8; } - /* - if (isAnimated) + if (hasAnimation) { // Set animated flag. flags |= 2; } - */ if (xmpProfile != null) { @@ -386,7 +374,7 @@ internal abstract class BitWriterBase flags |= 32; } - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 4d526e7b4b..5859d8a872 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -267,7 +267,7 @@ internal class Vp8LEncoder : IDisposable this.EncodeStream(image.Frames.RootFrame); this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( + BitWriterBase.WriteTrunksBeforeData( stream, (uint)width, (uint)height, @@ -275,13 +275,12 @@ internal class Vp8LEncoder : IDisposable xmpProfile, metadata.IccProfile, false /*hasAlpha*/, - Span.Empty, false); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f744827bf3..ccd7d8b6d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -88,7 +88,8 @@ internal class Vp8Encoder : IDisposable private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; - private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; + private const long HeaderSizeEstimate = + WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; private const int QMin = 0; @@ -165,7 +166,7 @@ internal class Vp8Encoder : IDisposable // TODO: make partition_limit configurable? const int limit = 100; // original code: limit = 100 - config->partition_limit; this.maxI4HeaderBits = - 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. + 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh]; for (int i = 0; i < this.MbInfo.Length; i++) @@ -308,22 +309,88 @@ internal class Vp8Encoder : IDisposable /// private int MbHeaderLimit { get; } + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + hasAlpha, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void EncodeAnimation(ImageFrame frame, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(frame, stream, true, null); + /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeStatic(Image image, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(image.Frames.RootFrame, stream, false, image); + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + /// The to encode from. + private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; + int pixelCount = width * height; Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v); + + if (!hasAnimation) + { + this.EncodeHeader(image, stream, hasAlpha, false); + } int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -375,13 +442,6 @@ internal class Vp8Encoder : IDisposable // Store filter stats. this.AdjustFilterStrength(); - // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - - ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; - XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - // Extract and encode alpha channel data, if present. int alphaDataSize = 0; bool alphaCompressionSucceeded = false; @@ -393,7 +453,7 @@ internal class Vp8Encoder : IDisposable { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image.Frames.RootFrame, + frame, this.configuration, this.memoryAllocator, this.skipMetadata, @@ -409,20 +469,31 @@ internal class Vp8Encoder : IDisposable } this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - hasAlpha, - alphaData[..alphaDataSize], - this.alphaCompression && alphaCompressionSucceeded); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } + + if (hasAlpha) + { + Span data = alphaData[..alphaDataSize]; + bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded; + BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed); + } this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } finally { diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 49512e03b5..2751f99134 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -144,7 +144,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder enc = new( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -156,7 +156,33 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); - enc.Encode(image, stream); + if (image.Frames.Count > 1) + { + encoder.EncodeHeader(image, stream, false, true); + + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8Encoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); + } + } + else + { + encoder.EncodeStatic(image, stream); + } + + encoder.EncodeFooter(image, stream); } } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 6c5fa50ff6..4b100e854e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -17,6 +17,13 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); + [Fact] + public void Encode_AnimatedLossy() + { + Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + } + [Theory] [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] From 87de52141219f23a39559a8bae09d79c30e109d6 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:58:56 +0800 Subject: [PATCH 27/44] Implement Vp8L encoder --- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 87 +++++++++++++------ .../Formats/Webp/WebpEncoderCore.cs | 32 ++++++- .../Formats/WebP/WebpEncoderTests.cs | 7 +- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 5859d8a872..d301df94f6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -236,26 +235,59 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - /// - /// Encodes the image as lossless webp to the specified stream. - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - + // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image as lossless webp to the specified stream. + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + public void Encode(ImageFrame frame, Stream stream, bool hasAnimation) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(image.Frames.RootFrame, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -264,23 +296,28 @@ internal class Vp8LEncoder : IDisposable this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image.Frames.RootFrame); + this.EncodeStream(frame); this.bitWriter.Finish(); - BitWriterBase.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - false /*hasAlpha*/, - false); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } /// @@ -1843,9 +1880,9 @@ internal class Vp8LEncoder : IDisposable { this.Bgra.Dispose(); this.EncodedData.Dispose(); - this.BgraScratch.Dispose(); + this.BgraScratch?.Dispose(); this.Palette.Dispose(); - this.TransformData.Dispose(); + this.TransformData?.Dispose(); this.HashChain.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 2751f99134..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder enc = new( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -140,7 +140,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.transparentColorMode, this.nearLossless, this.nearLosslessQuality); - enc.Encode(image, stream); + + bool hasAnimation = image.Frames.Count > 1; + encoder.EncodeHeader(image, stream, hasAnimation); + if (hasAnimation) + { + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8LEncoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + + enc.Encode(imageFrame, stream, true); + } + } + else + { + encoder.Encode(image.Frames.RootFrame, stream, false); + } + + encoder.EncodeFooter(image, stream); } else { @@ -174,6 +201,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 4b100e854e..1721cd938b 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -20,8 +20,11 @@ public class WebpEncoderTests [Fact] public void Encode_AnimatedLossy() { - Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless + }); } [Theory] From 2c260b27bf273f7154a93a43bf1e5149776fbe5d Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 00:09:10 +0800 Subject: [PATCH 28/44] add unit test and format --- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 6 ++-- .../Formats/WebP/WebpEncoderTests.cs | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index bce77c9e5c..0b71a3ed0c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -44,9 +44,6 @@ internal class Vp8LBitWriter : BitWriterBase { } - /// - public override int NumBytes => this.cur + ((this.used + 7) >> 3); - /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -59,6 +56,9 @@ internal class Vp8LBitWriter : BitWriterBase this.cur = cur; } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// This function writes bits into bytes in increasing addresses (little endian), /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go. diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 1721cd938b..d81c9eb93a 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -17,14 +18,28 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); - [Fact] - public void Encode_AnimatedLossy() + [Theory] + [WithFile(Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossless(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() - { - FileFormat = WebpFileFormatType.Lossless - }); + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + + // TODO: DebugSave, VerifySimilarity + } + + [Theory] + [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossy(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new()); + + // TODO: DebugSave, VerifySimilarity } [Theory] From 56588d3a1a612b64062f676015a12cc21106a47f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Oct 2023 09:23:08 +1000 Subject: [PATCH 29/44] 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 @@ internal readonly struct 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 6fed95b5165f4216d9cb476d54fbd3e7faf5f59b Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 09:03:23 +0800 Subject: [PATCH 30/44] remove unused scratchBuffer field --- src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4a9da3cbb1..89db7ed645 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -22,11 +22,6 @@ internal abstract class BitWriterBase /// private byte[] buffer; - /// - /// A scratch buffer to reduce allocations. - /// - private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly - /// /// Initializes a new instance of the class. /// From dbb89603ee4ac639f8b6fa08fde747250b10d7b5 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 12:38:17 +0800 Subject: [PATCH 31/44] encode FrameDuration --- src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs | 3 ++- src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d301df94f6..af472845ac 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -307,7 +307,8 @@ internal class Vp8LEncoder : IDisposable prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ccd7d8b6d5..40dbb90de6 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -477,7 +477,8 @@ internal class Vp8Encoder : IDisposable prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } From bd2d4550a998bb521cc8cce39bad9be42da201bf Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 16:47:14 +0800 Subject: [PATCH 32/44] refactor to follow style rules --- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 2 +- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 17 ++---- .../Formats/Webp/AnimationFrameData.cs | 2 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 4 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 2 +- .../Webp/Lossless/BackwardReferenceEncoder.cs | 2 +- .../Formats/Webp/Lossless/CostManager.cs | 2 +- .../Formats/Webp/Lossless/PixOrCopy.cs | 7 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 18 +++--- .../Webp/Lossless/WebpLosslessDecoder.cs | 4 +- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 6 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 16 ++--- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 18 +++--- .../Formats/Webp/WebpAnimationDecoder.cs | 12 ++-- .../Formats/Webp/WebpAnimationEncoder.cs | 12 ---- .../Formats/Webp/WebpChunkParsingUtils.cs | 20 ++++--- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 9 ++- .../Formats/Webp/WebpDecoderCore.cs | 27 +++++---- .../Formats/Webp/WebpDecoderOptions.cs | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 58 ++++--------------- src/ImageSharp/Formats/Webp/WebpFormat.cs | 6 +- 22 files changed, 99 insertions(+), 149 deletions(-) delete mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index 289ebd35ca..63e6541354 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -59,7 +59,7 @@ internal class AlphaDecoder : IDisposable if (this.Compressed) { - Vp8LBitReader bitReader = new(data); + Vp8LBitReader bitReader = new Vp8LBitReader(data); this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index a18d44fde4..2084686969 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,17 +43,8 @@ internal static class AlphaEncoder { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new( - memoryAllocator, - configuration, - width, - height, - quality, - skipMetadata, - effort, - WebpTransparentColorMode.Preserve, - false, - 0); + using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, + skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, @@ -81,7 +72,7 @@ internal static class AlphaEncoder { int width = frame.Width; int height = frame.Height; - ImageFrame alphaAsFrame = new(Configuration.Default, width, height); + ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height); for (int y = 0; y < height; y++) { @@ -91,7 +82,7 @@ internal static class AlphaEncoder for (int x = 0; x < width; x++) { // Leave A/R/B channels zero'd. - pixelRow[x] = new(0, alphaRow[x], 0, 0); + pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); } } diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs index 3400fef17d..27a1815fe3 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs @@ -62,7 +62,7 @@ internal struct AnimationFrameData { Span buffer = stackalloc byte[4]; - AnimationFrameData data = new() + AnimationFrameData data = new AnimationFrameData { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 923d2a69c4..81530706d6 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -399,7 +399,7 @@ internal class Vp8BitWriter : BitWriterBase int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); - Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); + Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(expectedSize, this.enc); // Partition #0 with header and partition sizes. uint size0 = bitWriterPartZero.GeneratePartition0(); @@ -545,7 +545,7 @@ internal class Vp8BitWriter : BitWriterBase // Writes the partition #0 modes (that is: all intra modes) private void CodeIntraModes() { - Vp8EncIterator it = new(this.enc); + Vp8EncIterator it = new Vp8EncIterator(this.enc); int predsWidth = this.enc.PredsWidth; do diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0b71a3ed0c..dc867fa85e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -102,7 +102,7 @@ internal class Vp8LBitWriter : BitWriterBase { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new(clonedBuffer, this.bits, this.used, this.cur); + return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 61133142bf..03cb1990b9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -775,7 +775,7 @@ internal static class BackwardReferenceEncoder private static void BackwardRefsWithLocalCache(ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) { int pixelIndex = 0; - ColorCache colorCache = new(cacheBits); + ColorCache colorCache = new ColorCache(cacheBits); for (int idx = 0; idx < refs.Refs.Count; idx++) { PixOrCopy v = refs.Refs[idx]; diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs index e393c065ec..63ce9dbec6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs @@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable private const int FreeIntervalsStartCount = 25; - private readonly Stack freeIntervals = new(FreeIntervalsStartCount); + private readonly Stack freeIntervals = new Stack(FreeIntervalsStartCount); public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner distArray, int pixCount, CostModel costModel) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index 6a28e5b3fb..61804812d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -15,7 +15,7 @@ internal sealed class PixOrCopy public uint BgraOrDistance { get; set; } public static PixOrCopy CreateCacheIdx(int idx) => - new() + new PixOrCopy { Mode = PixOrCopyMode.CacheIdx, BgraOrDistance = (uint)idx, @@ -23,14 +23,15 @@ internal sealed class PixOrCopy }; public static PixOrCopy CreateLiteral(uint bgra) => - new() + new PixOrCopy { Mode = PixOrCopyMode.Literal, BgraOrDistance = bgra, Len = 1 }; - public static PixOrCopy CreateCopy(uint distance, ushort len) => new() + public static PixOrCopy CreateCopy(uint distance, ushort len) => + new PixOrCopy { Mode = PixOrCopyMode.Copy, BgraOrDistance = distance, diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index af472845ac..3da27229ab 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -304,7 +304,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -547,7 +547,7 @@ internal class Vp8LEncoder : IDisposable EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new(); + List crunchConfigs = new List(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +641,8 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new(cacheBits); - List histogramImage = new(histogramImageXySize); + Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); + List histogramImage = new List(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,9 +839,9 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new() + List histogramImage = new List { - new(cacheBits) + new Vp8LHistogram(cacheBits) }; // Build histogram image and symbols from backward references. @@ -941,7 +941,7 @@ internal class Vp8LEncoder : IDisposable int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new() + HuffmanTreeCode huffmanCode = new HuffmanTreeCode { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1192,7 @@ internal class Vp8LEncoder : IDisposable histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new(); + Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1318,7 @@ internal class Vp8LEncoder : IDisposable /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new(); + HashSet colors = new HashSet(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 19ea424199..54dd1d6ed1 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,7 +95,7 @@ internal sealed class WebpLosslessDecoder public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator)) + using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) { this.DecodeImageStream(decoder, width, height, true); this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); @@ -616,7 +616,7 @@ internal sealed class WebpLosslessDecoder private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new(transformType, xSize, ySize); + Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); // Each transform is allowed to be used only once. foreach (Vp8LTransform decoderTransform in decoder.Transforms) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index a7c96edb7c..52c7e9703b 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -396,7 +396,7 @@ internal class Vp8EncIterator this.MakeLuma16Preds(); for (mode = 0; mode < maxMode; mode++) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) @@ -414,7 +414,7 @@ internal class Vp8EncIterator { Span modes = stackalloc byte[16]; const int maxMode = MaxIntra4Mode; - Vp8Histogram totalHisto = new(); + Vp8Histogram totalHisto = new Vp8Histogram(); int curHisto = 0; this.StartI4(); do @@ -467,7 +467,7 @@ internal class Vp8EncIterator this.MakeChroma8Preds(); for (mode = 0; mode < maxMode; ++mode) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 40dbb90de6..e62eb6cfc3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -395,7 +395,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +416,8 @@ internal class Vp8Encoder : IDisposable this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new(); - Vp8Residual residual = new(); + Vp8ModeScore info = new Vp8ModeScore(); + Vp8Residual residual = new Vp8Residual(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,7 +474,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -529,7 +529,7 @@ internal class Vp8Encoder : IDisposable Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +597,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +605,7 @@ internal class Vp8Encoder : IDisposable it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new(); + Vp8ModeScore info = new Vp8ModeScore(); do { info.Clear(); @@ -1167,7 +1167,7 @@ internal class Vp8Encoder : IDisposable private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new(); + Vp8Residual residual = new Vp8Residual(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 7952b15b44..4ac516f055 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ internal sealed class WebpLossyDecoder // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new() + Vp8PictureHeader pictureHeader = new Vp8PictureHeader { Width = (uint)width, Height = (uint)height, @@ -73,10 +73,11 @@ internal sealed class WebpLossyDecoder }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new(); + Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator)) + using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -101,13 +102,8 @@ internal sealed class WebpLossyDecoder if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, + info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); @@ -1062,7 +1058,7 @@ internal sealed class WebpLossyDecoder private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new() + Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader { UseSegment = this.bitReader.ReadBool() }; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 81a7aebdf9..6922e37d6e 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -153,7 +153,7 @@ internal class WebpAnimationDecoder : IDisposable } WebpImageInfo? webpInfo = null; - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -178,7 +178,7 @@ internal class WebpAnimationDecoder : IDisposable ImageFrame imageFrame; if (previousFrame is null) { - image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -259,19 +259,21 @@ internal class WebpAnimationDecoder : IDisposable private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); try { Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = + new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { - WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = + new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs deleted file mode 100644 index bfa64b6797..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Encoder for animated webp images. -/// -public class WebpAnimationEncoder -{ - // 可能不需要这个屌东西 -} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 9e9f0f7f62..f4e40090cf 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -8,6 +8,8 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -104,16 +106,16 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new() + Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -137,7 +139,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -166,7 +168,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -229,7 +231,7 @@ internal static class WebpChunkParsingUtils uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new() + WebpImageInfo info = new WebpImageInfo { Width = width, Height = height, @@ -291,7 +293,7 @@ internal static class WebpChunkParsingUtils if (stream.Read(buffer) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; + return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); @@ -348,7 +350,7 @@ internal static class WebpChunkParsingUtils if (metadata.ExifProfile != null) { - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } break; @@ -362,7 +364,7 @@ internal static class WebpChunkParsingUtils if (metadata.XmpProfile != null) { - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index e23b817ccd..dfbf4ef0e6 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -17,7 +17,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder /// /// Gets the shared instance. /// - public static WebpDecoder Instance { get; } = new(); + public static WebpDecoder Instance { get; } = new WebpDecoder(); /// protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -25,7 +25,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options }); + using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options }); return decoder.Identify(options.Configuration, stream, cancellationToken); } @@ -35,7 +35,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(options); + using WebpDecoderCore decoder = new WebpDecoderCore(options); Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); ScaleToTargetSize(options.GeneralOptions, image); @@ -52,6 +52,5 @@ public sealed class WebpDecoder : SpecializedImageDecoder => this.Decode(options, stream, cancellationToken); /// - protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) - => new() { GeneralOptions = options }; + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 63d3e1aead..bb54d99a04 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -8,7 +8,9 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -71,7 +73,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable public DecoderOptions Options { get; } /// - public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -80,7 +82,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Image? image = null; try { - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -89,7 +91,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, + this.configuration, this.maxFrames, this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -97,12 +100,14 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, + this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -127,12 +132,12 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), - new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), + new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), metadata); } } @@ -173,7 +178,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -327,7 +332,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return; } - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } } @@ -354,7 +359,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return; } - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } } @@ -380,7 +385,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new(iccpData); + IccProfile profile = new IccProfile(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs index 6fb15acbb4..8840805b1f 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpDecoderOptions : ISpecializedDecoderOptions { /// - public DecoderOptions GeneralOptions { get; init; } = new(); + public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); /// /// Gets the flag to decide how to handle the background color Animation Chunk. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 29d0c9e3b0..13c9798dbb 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -82,7 +82,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new(this, image.Configuration); + WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 47712071bf..dcff53f3af 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,17 +129,9 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -147,17 +139,9 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -171,36 +155,18 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, + this.spatialNoiseShaping, this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, + this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs index 29c74b11bf..197041234e 100644 --- a/src/ImageSharp/Formats/Webp/WebpFormat.cs +++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs @@ -15,7 +15,7 @@ public sealed class WebpFormat : IImageFormat /// /// Gets the shared instance. /// - public static WebpFormat Instance { get; } = new(); + public static WebpFormat Instance { get; } = new WebpFormat(); /// public string Name => "Webp"; @@ -30,8 +30,8 @@ public sealed class WebpFormat : IImageFormat public IEnumerable FileExtensions => WebpConstants.FileExtensions; /// - public WebpMetadata CreateDefaultFormatMetadata() => new(); + public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata(); /// - public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata(); } From d4483217b623aa751f5591d7edc93a7c812a91b2 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 19:48:07 +0800 Subject: [PATCH 33/44] fix SA1117 --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 13 ++++- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 15 ++++- .../Formats/Webp/WebpDecoderCore.cs | 19 ++++-- .../Formats/Webp/WebpEncoderCore.cs | 58 +++++++++++++++---- 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 2084686969..cbd2aa8e7f 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,8 +43,17 @@ internal static class AlphaEncoder { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, - skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); + using Vp8LEncoder lossLessEncoder = new( + memoryAllocator, + configuration, + width, + height, + quality, + skipMetadata, + effort, + WebpTransparentColorMode.Preserve, + false, + 0); // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 4ac516f055..354bcdbb44 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -76,7 +76,11 @@ internal sealed class WebpLossyDecoder Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + using (Vp8Decoder decoder = new Vp8Decoder( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -102,8 +106,13 @@ internal sealed class WebpLossyDecoder if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, - info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bb54d99a04..bc875c8890 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -91,8 +91,11 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, - this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -100,14 +103,18 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, - this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, + this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, - this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + this.webImageInfo.Vp8BitReader, + this.memoryAllocator, + this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index dcff53f3af..d945cc3990 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,9 +129,17 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -139,9 +147,17 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -155,18 +171,36 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, - this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, - this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } From 66f444d200985720a9ff3e27a7a52d4722836b43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 30 Oct 2023 23:52:41 +1000 Subject: [PATCH 34/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } } + + blendMemory?.Dispose(); } /// @@ -919,26 +943,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals 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 partial class PngDecoderTests } [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 TestImages 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 class ImagingTestCaseUtility 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 class ImagingTestCaseUtility 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 class ImagingTestCaseUtility { 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 @@ public class ImagingTestCaseUtility => 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 class TestImageExtensions 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 class TestImageExtensions 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 class TestImageProviderTests 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 35/44] 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 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { this.animationControl = AnimationControl.Parse(data); - pngMetadata.NumberPlays = this.animationControl.NumberPlays; + pngMetadata.RepeatCount = this.animationControl.NumberPlays; } /// 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 class PngFrameMetadata : IDeepCloneable /// 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 @@ public class PngFrameMetadata : IDeepCloneable /// 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 @@ public class PngMetadata : IDeepCloneable 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 @@ public class PngMetadata : IDeepCloneable public IList TextData { get; set; } = new List(); /// - /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. 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 readonly struct Rational : IEquatable /// 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 readonly struct Rational : IEquatable /// /// 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 readonly struct Rational : IEquatable /// /// 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 readonly struct Rational : IEquatable /// 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 partial class PngEncoderTests 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 class PngMetadataTests 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 class PngMetadataTests 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 36/44] 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 }; From b89bc54aa20b7868010d34734bd1a6ea88be92a2 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 31 Oct 2023 18:27:35 +0800 Subject: [PATCH 37/44] Add new member to WebpFrameMetadata --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 8 +++-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 30 +++++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 26 +++++++++------- .../Formats/Webp/WebpAnimationDecoder.cs | 29 +++++++++--------- ...lendingMethod.cs => WebpBlendingMethod.cs} | 2 +- .../Formats/Webp/WebpDecoderCore.cs | 16 +++++----- ...isposalMethod.cs => WebpDisposalMethod.cs} | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 4 +-- .../Formats/Webp/WebpEncoderCore.cs | 8 ++--- ...AnimationFrameData.cs => WebpFrameData.cs} | 14 ++++----- .../Formats/Webp/WebpFrameMetadata.cs | 19 ++++++++++-- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 9 ++++++ .../Formats/WebP/WebpDecoderTests.cs | 4 +-- 13 files changed, 102 insertions(+), 69 deletions(-) rename src/ImageSharp/Formats/Webp/{AnimationBlendingMethod.cs => WebpBlendingMethod.cs} (95%) rename src/ImageSharp/Formats/Webp/{AnimationDisposalMethod.cs => WebpDisposalMethod.cs} (94%) rename src/ImageSharp/Formats/Webp/{AnimationFrameData.cs => WebpFrameData.cs} (83%) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 89db7ed645..c1860c9c59 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -231,14 +231,14 @@ internal abstract class BitWriterBase /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); stream.Write(buf); BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); stream.Write(buf[..2]); @@ -249,7 +249,7 @@ internal abstract class BitWriterBase /// /// The stream to write to. /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) + public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); @@ -262,6 +262,8 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 3da27229ab..9156d5bdf8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -235,7 +235,7 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -257,7 +257,8 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -304,11 +305,14 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -547,7 +551,7 @@ internal class Vp8LEncoder : IDisposable EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new List(); + List crunchConfigs = new(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +645,8 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); - List histogramImage = new List(histogramImageXySize); + Vp8LHistogram tmpHisto = new(cacheBits); + List histogramImage = new(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,7 +843,7 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new List + List histogramImage = new() { new Vp8LHistogram(cacheBits) }; @@ -941,7 +945,7 @@ internal class Vp8LEncoder : IDisposable int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new HuffmanTreeCode + HuffmanTreeCode huffmanCode = new() { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1196,7 @@ internal class Vp8LEncoder : IDisposable histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); + Vp8LBitEntropy bitEntropy = new(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1322,7 @@ internal class Vp8LEncoder : IDisposable /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new HashSet(); + HashSet colors = new(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); @@ -1870,9 +1874,9 @@ internal class Vp8LEncoder : IDisposable /// public void ClearRefs() { - for (int i = 0; i < this.Refs.Length; i++) + foreach (Vp8LBackwardRefs t in this.Refs) { - this.Refs[i].Refs.Clear(); + t.Refs.Clear(); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index e62eb6cfc3..3a6e9a2ccd 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -309,7 +309,7 @@ internal class Vp8Encoder : IDisposable /// private int MbHeaderLimit { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -331,7 +331,8 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -395,7 +396,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +417,8 @@ internal class Vp8Encoder : IDisposable this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new Vp8ModeScore(); - Vp8Residual residual = new Vp8Residual(); + Vp8ModeScore info = new(); + Vp8Residual residual = new(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,11 +475,14 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -529,7 +533,7 @@ internal class Vp8Encoder : IDisposable Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +601,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +609,7 @@ internal class Vp8Encoder : IDisposable it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new Vp8ModeScore(); + Vp8ModeScore info = new(); do { info.Clear(); @@ -1167,7 +1171,7 @@ internal class Vp8Encoder : IDisposable private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new Vp8Residual(); + Vp8Residual residual = new(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 6922e37d6e..87657dfabb 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = AnimationFrameData.Parse(stream); + WebpFrameData frameData = WebpFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -153,7 +153,7 @@ internal class WebpAnimationDecoder : IDisposable } WebpImageInfo? webpInfo = null; - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -180,7 +180,7 @@ internal class WebpAnimationDecoder : IDisposable { image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); - SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); + SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData); imageFrame = image.Frames.RootFrame; } @@ -188,7 +188,7 @@ internal class WebpAnimationDecoder : IDisposable { currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection. - SetFrameMetadata(currentFrame.Metadata, frameData.Duration); + SetFrameMetadata(currentFrame.Metadata, frameData); imageFrame = currentFrame; } @@ -199,7 +199,7 @@ internal class WebpAnimationDecoder : IDisposable int frameHeight = (int)frameData.Height; Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); - if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose) + if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { this.RestoreToBackground(imageFrame, backgroundColor); } @@ -207,7 +207,7 @@ internal class WebpAnimationDecoder : IDisposable using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); - if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) + if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); } @@ -222,12 +222,13 @@ internal class WebpAnimationDecoder : IDisposable /// Sets the frames metadata. /// /// The metadata. - /// The frame duration. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration) + /// The frame data. + private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData) { WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); - frameMetadata.FrameDuration = duration; + frameMetadata.FrameDelay = frameData.Duration; + frameMetadata.BlendMethod = frameData.BlendingMethod; + frameMetadata.DisposalMethod = frameData.DisposalMethod; } /// @@ -256,10 +257,10 @@ internal class WebpAnimationDecoder : IDisposable /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new((int)frameData.Width, (int)frameData.Height); try { @@ -267,13 +268,13 @@ internal class WebpAnimationDecoder : IDisposable if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = - new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { WebpLossyDecoder lossyDecoder = - new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs similarity index 95% rename from src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs rename to src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs index 99b2462cea..cbd0e9a8cc 100644 --- a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// -internal enum AnimationBlendingMethod +public enum WebpBlendingMethod { /// /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bc875c8890..de188b137b 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -73,7 +73,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable public DecoderOptions Options { get; } /// - public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -82,7 +82,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Image? image = null; try { - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -91,7 +91,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + using WebpAnimationDecoder animationDecoder = new( this.memoryAllocator, this.configuration, this.maxFrames, @@ -103,7 +103,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + WebpLosslessDecoder losslessDecoder = new( this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -111,7 +111,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + WebpLossyDecoder lossyDecoder = new( this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); @@ -139,7 +139,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( @@ -185,7 +185,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -392,7 +392,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new IccProfile(iccpData); + IccProfile profile = new(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs similarity index 94% rename from src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs rename to src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs index 23bc37c283..d409973a99 100644 --- a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// -internal enum AnimationDisposalMethod +public enum WebpDisposalMethod { /// /// Do not dispose. Leave the canvas as is. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 13c9798dbb..bc93df3a5b 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Webp; /// @@ -82,7 +80,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); + WebpEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d945cc3990..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -147,7 +147,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder( + using Vp8LEncoder enc = new( this.memoryAllocator, this.configuration, image.Width, @@ -171,7 +171,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new Vp8Encoder( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -189,7 +189,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder( + using Vp8Encoder enc = new( this.memoryAllocator, this.configuration, image.Width, diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs similarity index 83% rename from src/ImageSharp/Formats/Webp/AnimationFrameData.cs rename to src/ImageSharp/Formats/Webp/WebpFrameData.cs index 27a1815fe3..e2bcfd7c36 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Webp; -internal struct AnimationFrameData +internal struct WebpFrameData { /// /// The animation chunk size. @@ -46,23 +46,23 @@ internal struct AnimationFrameData /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// - public AnimationBlendingMethod BlendingMethod; + public WebpBlendingMethod BlendingMethod; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// - public AnimationDisposalMethod DisposalMethod; + public WebpDisposalMethod DisposalMethod; /// /// Reads the animation frame header. /// /// The stream to read from. /// Animation frame data. - public static AnimationFrameData Parse(BufferedReadStream stream) + public static WebpFrameData Parse(BufferedReadStream stream) { Span buffer = stackalloc byte[4]; - AnimationFrameData data = new AnimationFrameData + WebpFrameData data = new() { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), @@ -83,8 +83,8 @@ internal struct AnimationFrameData }; byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; return data; } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index bce1b09d6f..ef21d8b6fe 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -19,13 +19,28 @@ public class WebpFrameMetadata : IDeepCloneable /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; + private WebpFrameMetadata(WebpFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; + } + + /// + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendMethod { get; set; } + + /// + /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined. /// - public uint FrameDuration { get; set; } + public uint FrameDelay { get; set; } /// public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 5d1051c751..a6bb0a7b80 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -23,6 +23,7 @@ public class WebpMetadata : IDeepCloneable { this.FileFormat = other.FileFormat; this.AnimationLoopCount = other.AnimationLoopCount; + this.AnimationBackground = other.AnimationBackground; } /// @@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable /// public ushort AnimationLoopCount { get; set; } = 1; + /// + /// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public Color AnimationBackground { get; set; } + /// public IDeepCloneable DeepClone() => new WebpMetadata(this); } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c0fc00b82d..c3a777c153 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -308,7 +308,7 @@ public class WebpDecoderTests image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); Assert.Equal(0, webpMetaData.AnimationLoopCount); - Assert.Equal(150U, frameMetaData.FrameDuration); + Assert.Equal(150U, frameMetaData.FrameDelay); Assert.Equal(12, image.Frames.Count); } @@ -325,7 +325,7 @@ public class WebpDecoderTests image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f)); Assert.Equal(0, webpMetaData.AnimationLoopCount); - Assert.Equal(150U, frameMetaData.FrameDuration); + Assert.Equal(150U, frameMetaData.FrameDelay); Assert.Equal(12, image.Frames.Count); } From b4e1b7f4e105360698fa155257c8d72c59bf52e2 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 1 Nov 2023 22:46:20 +0800 Subject: [PATCH 38/44] fix --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 1 - .../Formats/Webp/Lossless/Vp8LEncoder.cs | 4 + .../Webp/Lossless/WebpLosslessDecoder.cs | 115 +++++----- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 + .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 198 +++++++++--------- .../Formats/Webp/WebpAnimationDecoder.cs | 53 ++--- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 2 + 7 files changed, 188 insertions(+), 189 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index c1860c9c59..cbf96a91af 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -263,7 +263,6 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9156d5bdf8..42aa667ac5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -306,8 +306,12 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 54dd1d6ed1..e4c2a7ddf6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,12 +95,10 @@ internal sealed class WebpLosslessDecoder public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) - { - this.DecodeImageStream(decoder, width, height, true); - this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); - this.DecodePixelValues(decoder, pixels, width, height); - } + using Vp8LDecoder decoder = new(width, height, this.memoryAllocator); + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels, width, height); } public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) @@ -616,15 +614,12 @@ internal sealed class WebpLosslessDecoder private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); + Vp8LTransform transform = new(transformType, xSize, ySize); // Each transform is allowed to be used only once. - foreach (Vp8LTransform decoderTransform in decoder.Transforms) + if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType)) { - if (decoderTransform.TransformType == transform.TransformType) - { - WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); - } + WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); } switch (transformType) @@ -744,61 +739,69 @@ internal sealed class WebpLosslessDecoder this.bitReader.FillBitWindow(); int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); - if (code < WebpConstants.NumLiteralCodes) + switch (code) { - // Literal - data[pos] = (byte)code; - ++pos; - ++col; - - if (col >= width) + case < WebpConstants.NumLiteralCodes: { - col = 0; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) { - dec.ExtractPalettedAlphaRows(row); + col = 0; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } } - } - } - else if (code < lenCodeLimit) - { - // Backward reference - int lengthSym = code - WebpConstants.NumLiteralCodes; - int length = this.GetCopyLength(lengthSym); - int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); - this.bitReader.FillBitWindow(); - int distCode = this.GetCopyDistance(distSymbol); - int dist = PlaneCodeToDistance(width, distCode); - if (pos >= dist && end - pos >= length) - { - CopyBlock8B(data, pos, dist, length); - } - else - { - WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + + break; } - pos += length; - col += length; - while (col >= width) + case < lenCodeLimit: { - col -= width; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Backward reference + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) { - dec.ExtractPalettedAlphaRows(row); + CopyBlock8B(data, pos, dist, length); + } + else + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); } - } - if (pos < last && (col & mask) > 0) - { - htreeGroup = GetHTreeGroupForPos(hdr, col, row); + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + + break; } - } - else - { - WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + + default: + WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + break; } this.bitReader.Eos = this.bitReader.IsEndOfStream(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3a6e9a2ccd..3b73023062 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -476,8 +476,12 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 354bcdbb44..3eb03b1724 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ internal sealed class WebpLossyDecoder // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new Vp8PictureHeader + Vp8PictureHeader pictureHeader = new() { Width = (uint)width, Height = (uint)height, @@ -73,55 +73,51 @@ internal sealed class WebpLossyDecoder }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new Vp8Proba(); + Vp8Proba proba = new(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder( - info.Vp8FrameHeader, - pictureHeader, - vp8SegmentHeader, - proba, - this.memoryAllocator)) - { - Vp8Io io = InitializeVp8Io(decoder, pictureHeader); + using Vp8Decoder decoder = new( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, + this.memoryAllocator); + Vp8Io io = InitializeVp8Io(decoder, pictureHeader); - // Paragraph 9.4: Parse the filter specs. - this.ParseFilterHeader(decoder); - decoder.PrecomputeFilterStrengths(); + // Paragraph 9.4: Parse the filter specs. + this.ParseFilterHeader(decoder); + decoder.PrecomputeFilterStrengths(); - // Paragraph 9.5: Parse partitions. - this.ParsePartitions(decoder); + // Paragraph 9.5: Parse partitions. + this.ParsePartitions(decoder); - // Paragraph 9.6: Dequantization Indices. - this.ParseDequantizationIndices(decoder); + // Paragraph 9.6: Dequantization Indices. + this.ParseDequantizationIndices(decoder); - // Ignore the value of update probabilities. - this.bitReader.ReadBool(); + // Ignore the value of update probabilities. + this.bitReader.ReadBool(); - // Paragraph 13.4: Parse probabilities. - this.ParseProbabilities(decoder); + // Paragraph 13.4: Parse probabilities. + this.ParseProbabilities(decoder); - // Decode image data. - this.ParseFrame(decoder, io); + // Decode image data. + this.ParseFrame(decoder, io); - if (info.Features?.Alpha == true) - { - using (AlphaDecoder alphaDecoder = new AlphaDecoder( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) - { - alphaDecoder.Decode(); - DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); - } - } - else - { - this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); - } + if (info.Features?.Alpha == true) + { + using AlphaDecoder alphaDecoder = new( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration); + alphaDecoder.Decode(); + DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); } } @@ -199,8 +195,8 @@ internal sealed class WebpLossyDecoder { // Hardcoded tree parsing. block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0 - ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) - : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); + ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) + : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); } else { @@ -595,57 +591,65 @@ internal sealed class WebpLossyDecoder return; } - if (dec.Filter == LoopFilter.Simple) + switch (dec.Filter) { - int offset = dec.CacheYOffset + (mbx * 16); - if (mbx > 0) + case LoopFilter.Simple: { - LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (mby > 0) - { - LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } - } - else if (dec.Filter == LoopFilter.Complex) - { - int uvBps = dec.CacheUvStride; - int yOffset = dec.CacheYOffset + (mbx * 16); - int uvOffset = dec.CacheUvOffset + (mbx * 8); - int hevThresh = filterInfo.HighEdgeVarianceThreshold; - if (mbx > 0) - { - LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + break; } - if (mby > 0) + case LoopFilter.Complex: { - LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + int uvBps = dec.CacheUvStride; + int yOffset = dec.CacheYOffset + (mbx * 16); + int uvOffset = dec.CacheUvOffset + (mbx * 8); + int hevThresh = filterInfo.HighEdgeVarianceThreshold; + if (mbx > 0) + { + LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + if (filterInfo.UseInnerFiltering) + { + LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + if (mby > 0) + { + LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + break; } } } @@ -1067,7 +1071,7 @@ internal sealed class WebpLossyDecoder private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader + Vp8SegmentHeader vp8SegmentHeader = new() { UseSegment = this.bitReader.ReadBool() }; @@ -1333,18 +1337,12 @@ internal sealed class WebpLossyDecoder private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) { nzCoeffs <<= 2; - if (nz > 3) + nzCoeffs |= nz switch { - nzCoeffs |= 3; - } - else if (nz > 1) - { - nzCoeffs |= 2; - } - else - { - nzCoeffs |= (uint)dcNz; - } + > 3 => 3, + > 1 => 2, + _ => (uint)dcNz + }; return nzCoeffs; } @@ -1358,13 +1356,13 @@ internal sealed class WebpLossyDecoder if (mbx == 0) { return mby == 0 - ? 6 // B_DC_PRED_NOTOPLEFT - : 5; // B_DC_PRED_NOLEFT + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT } return mby == 0 - ? 4 // B_DC_PRED_NOTOP - : 0; // B_DC_PRED + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED } return mode; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 87657dfabb..fad6ca16cc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -193,11 +192,7 @@ internal class WebpAnimationDecoder : IDisposable imageFrame = currentFrame; } - int frameX = (int)(frameData.X * 2); - int frameY = (int)(frameData.Y * 2); - int frameWidth = (int)frameData.Width; - int frameHeight = (int)frameData.Height; - Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); + Rectangle regionRectangle = frameData.Bounds; if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { @@ -205,11 +200,11 @@ internal class WebpAnimationDecoder : IDisposable } using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); + DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { - this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); + this.AlphaBlend(previousFrame, imageFrame, regionRectangle); } previousFrame = currentFrame ?? image.Frames.RootFrame; @@ -245,7 +240,7 @@ internal class WebpAnimationDecoder : IDisposable byte alphaChunkHeader = (byte)stream.ReadByte(); Span alphaData = this.alphaData.GetSpan(); - stream.Read(alphaData, 0, alphaDataSize); + _ = stream.Read(alphaData, 0, alphaDataSize); return alphaChunkHeader; } @@ -260,11 +255,11 @@ internal class WebpAnimationDecoder : IDisposable private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); try { - Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); + Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = @@ -282,7 +277,7 @@ internal class WebpAnimationDecoder : IDisposable } catch { - decodedImage?.Dispose(); + decodedFrame?.Dispose(); throw; } finally @@ -297,20 +292,17 @@ internal class WebpAnimationDecoder : IDisposable /// The type of the pixel. /// The decoded image. /// The image frame to draw into. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D imageFramePixels = imageFrame.PixelBuffer; + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); int decodedRowIdx = 0; - for (int y = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth]; - decodedPixelRow.TryCopyTo(framePixelRow[frameX..]); + Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; + decodedPixelRow.TryCopyTo(framePixelRow); } } @@ -321,22 +313,19 @@ internal class WebpAnimationDecoder : IDisposable /// The pixel format. /// The source image. /// The destination image. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private void AlphaBlend(ImageFrame src, ImageFrame dst, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + 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 = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); } } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs index e2bcfd7c36..93c5d10dcd 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -53,6 +53,8 @@ internal struct WebpFrameData /// public WebpDisposalMethod DisposalMethod; + public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + /// /// Reads the animation frame header. /// From 296da738008b06ea511b4e0123d059aa4e660a89 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 14:26:50 +0800 Subject: [PATCH 39/44] add riif helper --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/ImageSharp/Common/Helpers/RiffHelper.cs diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs new file mode 100644 index 0000000000..6354ebd663 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal class RiffHelper +{ + /// + /// The header bytes identifying RIFF file. + /// + public static readonly uint RiffFourCc = 0x52_49_46_46; + + public static void WriteRiffFile(Stream stream, string formType, Action func) => + WriteChunk(stream, RiffFourCc, s => + { + s.Write(Encoding.ASCII.GetBytes(formType)); + func(s); + }); + + public static void WriteChunk(Stream stream, uint fourCc, Action func) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + func(stream); + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } + + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 == 1) + { + stream.WriteByte(0); + } + } + + public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged + { + fixed (TStruct* ptr = &chunk) + { + WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); + } + } + + public static long BeginWriteChunk(Stream stream, uint fourCc) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + return sizePosition; + } + + public static void EndWriteChunk(Stream stream, long sizePosition) + { + Span buffer = stackalloc byte[4]; + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } +} From 9dc0cda95e1c810f3bd56db1803fc0aa6cea4a09 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 15:06:57 +0800 Subject: [PATCH 40/44] add static --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 6354ebd663..0395d9a9c9 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -6,12 +6,12 @@ using System.Text; namespace SixLabors.ImageSharp.Common.Helpers; -internal class RiffHelper +internal static class RiffHelper { /// /// The header bytes identifying RIFF file. /// - public static readonly uint RiffFourCc = 0x52_49_46_46; + private const uint RiffFourCc = 0x52_49_46_46; public static void WriteRiffFile(Stream stream, string formType, Action func) => WriteChunk(stream, RiffFourCc, s => @@ -64,7 +64,7 @@ internal class RiffHelper stream.Write(data); // padding - if (size % 2 == 1) + if (size % 2 is 1) { stream.WriteByte(0); } @@ -103,7 +103,7 @@ internal class RiffHelper uint dataSize = (uint)(position - sizePosition - 4); // padding - if (dataSize % 2 == 1) + if (dataSize % 2 is 1) { stream.WriteByte(0); position++; @@ -114,4 +114,13 @@ internal class RiffHelper stream.Position = position; } + + public static long BeginWriteRiffFile(Stream stream, string formType) + { + long sizePosition = BeginWriteChunk(stream, RiffFourCc); + stream.Write(Encoding.ASCII.GetBytes(formType)); + return sizePosition; + } + + public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition); } From 4beafcf65586ff2631fefd2d6b1aedc1422ab4c8 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 17:16:44 +0800 Subject: [PATCH 41/44] fix FourCC bug --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 0395d9a9c9..8f06e5886f 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -25,7 +25,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -34,7 +34,6 @@ internal static class RiffHelper func(stream); long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -46,8 +45,8 @@ internal static class RiffHelper } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } @@ -56,7 +55,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); uint size = (uint)data.Length; BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); @@ -84,7 +83,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -98,7 +97,6 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -110,8 +108,8 @@ internal static class RiffHelper } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } From 1b0b877a147e81b9c3ff9d3e90de668b4e13cf1d Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 19:18:56 +0800 Subject: [PATCH 42/44] introduce RiffHelper --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 222 ++---------------- .../Webp/Chunks/WebpAnimationParameter.cs | 37 +++ .../Formats/Webp/Chunks/WebpFrameData.cs | 140 +++++++++++ .../Formats/Webp/Chunks/WebpVp8X.cs | 113 +++++++++ .../Formats/Webp/Lossless/Vp8LEncoder.cs | 23 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 23 +- .../Formats/Webp/WebpAnimationDecoder.cs | 3 +- .../Formats/Webp/WebpChunkParsingUtils.cs | 18 +- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 2 +- src/ImageSharp/Formats/Webp/WebpConstants.cs | 10 +- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 93 -------- 11 files changed, 349 insertions(+), 335 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpFrameData.cs diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index cbf96a91af..d502fd6063 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -1,8 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Binary; -using System.Runtime.InteropServices; +using System.Diagnostics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -15,8 +16,6 @@ internal abstract class BitWriterBase private const ulong MaxCanvasPixels = 4294967295ul; - protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; - /// /// Buffer to write to. /// @@ -79,48 +78,6 @@ internal abstract class BitWriterBase Array.Resize(ref this.buffer, newSize); } - /// - /// Writes the RIFF header to the stream. - /// - /// The stream to write to. - /// The block length. - protected static void WriteRiffHeader(Stream stream, uint riffSize) - { - stream.Write(WebpConstants.RiffFourCc); - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); - stream.Write(buf); - stream.Write(WebpConstants.WebpHeader); - } - - /// - /// Calculates the chunk size of EXIF, XMP or ICCP metadata. - /// - /// The metadata profile bytes. - /// The metadata chunk size in bytes. - protected static uint MetadataChunkSize(byte[] metadataBytes) - { - uint metaSize = (uint)metadataBytes.Length; - return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1); - } - - /// - /// Calculates the chunk size of a alpha chunk. - /// - /// The alpha chunk bytes. - /// The alpha data chunk size in bytes. - protected static uint AlphaChunkSize(Span alphaBytes) - { - uint alphaSize = (uint)alphaBytes.Length + 1; - return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); - } - - /// - /// Overwrites ides the write file size. - /// - /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); - /// /// Write the trunks before data trunk. /// @@ -143,7 +100,9 @@ internal abstract class BitWriterBase bool hasAnimation) { // Write file size later - WriteRiffHeader(stream, 0); + long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); + + Debug.Assert(pos is 4, "Stream should be written from position 0."); // Write VP8X, header if necessary. bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; @@ -153,7 +112,7 @@ internal abstract class BitWriterBase if (iccProfile != null) { - WriteColorProfile(stream, iccProfile.ToByteArray()); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); } } } @@ -177,49 +136,17 @@ internal abstract class BitWriterBase { if (exifProfile != null) { - WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); } if (xmpProfile != null) { - WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - OverwriteFileSize(stream); - } - - /// - /// Writes a metadata profile (EXIF or XMP) to the stream. - /// - /// The stream to write to. - /// The metadata profile's bytes. - /// The chuck type to write. - protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) - { - DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); - - uint size = (uint)metadataBytes.Length; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - stream.Write(metadataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteRiffFile(stream, 4); } - /// - /// Writes the color profile() to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); - /// /// Writes the animation parameter() to the stream. /// @@ -233,55 +160,8 @@ internal abstract class BitWriterBase /// The number of times to loop the animation. If it is 0, this means infinitely. public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); - stream.Write(buf); - BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); - stream.Write(buf[..2]); - } - - /// - /// Writes the animation frame() to the stream. - /// - /// The stream to write to. - /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) - { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); - stream.Write(buf); - long position = stream.Position; - BinaryPrimitives.WriteUInt32BigEndian(buf, 0); - stream.Write(buf); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - - byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); - stream.WriteByte(flag); - return position; - } - - /// - /// Overwrites ides the write frame size. - /// - /// The stream to write to. - /// Previous position. - public static void OverwriteFrameSize(Stream stream, long prevPosition) - { - uint position = (uint)stream.Position; - stream.Position = prevPosition; - byte[] buffer = new byte[4]; - - BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); - stream.Write(buffer); - stream.Position = position; + WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount); + chunk.WriteTo(stream); } /// @@ -292,27 +172,17 @@ internal abstract class BitWriterBase /// Indicates, if the alpha channel data is compressed. public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { - uint size = (uint)dataBytes.Length + 1; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha); byte flags = 0; if (alphaDataIsCompressed) { + // TODO: Filtering and preprocessing flags = 1; } stream.WriteByte(flags); stream.Write(dataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteChunk(stream, pos); } /// @@ -328,66 +198,10 @@ internal abstract class BitWriterBase /// Flag indicating, if an animation parameter is present. protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { - if (width > MaxDimension || height > MaxDimension) - { - WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}"); - } + WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); - // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. - if (width * height > MaxCanvasPixels) - { - WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); - } - - uint flags = 0; - if (exifProfile != null) - { - // Set exif bit. - flags |= 8; - } - - if (hasAnimation) - { - // Set animated flag. - flags |= 2; - } - - if (xmpProfile != null) - { - // Set xmp bit. - flags |= 4; - } - - if (hasAlpha) - { - // Set alpha bit. - flags |= 16; - } - - if (iccProfile != null) - { - // Set iccp flag. - flags |= 32; - } - - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, width - 1); - stream.Write(buf[..3]); - BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); - stream.Write(buf[..3]); - } - - private unsafe struct ScratchBuffer - { - private const int Size = 4; - private fixed byte scratch[Size]; + chunk.Validate(MaxDimension, MaxCanvasPixels); - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + chunk.WriteTo(stream); } } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs new file mode 100644 index 0000000000..3855a293c1 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpAnimationParameter +{ + public WebpAnimationParameter(uint background, ushort loopCount) + { + this.Background = background; + this.LoopCount = loopCount; + } + + /// + /// Gets default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public uint Background { get; } + + /// + /// Gets number of times to loop the animation. If it is 0, this means infinitely. + /// + public ushort LoopCount { get; } + + public void WriteTo(Stream stream) + { + Span buffer = stackalloc byte[6]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background); + BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer); + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs new file mode 100644 index 0000000000..f22a3fd540 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpFrameData +{ + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + { + this.DataSize = dataSize; + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Duration = duration; + this.DisposalMethod = disposalMethod; + this.BlendingMethod = blendingMethod; + } + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, int flags) + : this( + dataSize, + x, + y, + width, + height, + duration, + (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending, + (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose) + { + } + + public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + : this(0, x, y, width, height, duration, blendingMethod, disposalMethod) + { + } + + /// + /// Gets the animation chunk size. + /// + public uint DataSize { get; } + + /// + /// Gets the X coordinate of the upper left corner of the frame is Frame X * 2. + /// + public uint X { get; } + + /// + /// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2. + /// + public uint Y { get; } + + /// + /// Gets the width of the frame. + /// + public uint Width { get; } + + /// + /// Gets the height of the frame. + /// + public uint Height { get; } + + /// + /// Gets the time to wait before displaying the next frame, in 1 millisecond units. + /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. + /// + public uint Duration { get; } + + /// + /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendingMethod { get; } + + /// + /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; } + + public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + public long WriteHeaderTo(Stream stream) + { + byte flags = 0; + + if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend) + { + // Set blending flag. + flags |= 2; + } + + if (this.DisposalMethod is WebpDisposalMethod.Dispose) + { + // Set disposal flag. + flags |= 1; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData); + + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration); + stream.WriteByte(flags); + + return pos; + } + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static WebpFrameData Parse(Stream stream) + { + Span buffer = stackalloc byte[4]; + + WebpFrameData data = new( + dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + flags: stream.ReadByte()); + + return data; + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs new file mode 100644 index 0000000000..70d6870ce4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpVp8X +{ + public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height) + { + this.HasAnimation = hasAnimation; + this.HasXmp = hasXmp; + this.HasExif = hasExif; + this.HasAlpha = hasAlpha; + this.HasIcc = hasIcc; + this.Width = width; + this.Height = height; + } + + /// + /// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation. + /// + public bool HasAnimation { get; } + + /// + /// Gets a value indicating whether the file contains XMP metadata. + /// + public bool HasXmp { get; } + + /// + /// Gets a value indicating whether the file contains Exif metadata. + /// + public bool HasExif { get; } + + /// + /// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha"). + /// + public bool HasAlpha { get; } + + /// + /// Gets a value indicating whether the file contains an 'ICCP' Chunk. + /// + public bool HasIcc { get; } + + /// + /// Gets width of the canvas in pixels. (uint24) + /// + public uint Width { get; } + + /// + /// Gets height of the canvas in pixels. (uint24) + /// + public uint Height { get; } + + public void Validate(uint maxDimension, ulong maxCanvasPixels) + { + if (this.Width > maxDimension || this.Height > maxDimension) + { + WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}"); + } + + // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. + if (this.Width * this.Height > maxCanvasPixels) + { + WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); + } + } + + public void WriteTo(Stream stream) + { + byte flags = 0; + + if (this.HasAnimation) + { + // Set animated flag. + flags |= 2; + } + + if (this.HasXmp) + { + // Set xmp bit. + flags |= 4; + } + + if (this.HasExif) + { + // Set exif bit. + flags |= 8; + } + + if (this.HasAlpha) + { + // Set alpha bit. + flags |= 16; + } + + if (this.HasIcc) + { + // Set icc flag. + flags |= 32; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X); + + stream.WriteByte(flags); + stream.Position += 3; // Reserved bytes + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + + RiffHelper.EndWriteChunk(stream, pos); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 42aa667ac5..fe0131a2aa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -6,7 +6,9 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -308,16 +310,15 @@ internal class Vp8LEncoder : IDisposable WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } // Write bytes from the bitwriter buffer to the stream. @@ -325,7 +326,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3b73023062..98e50bb9c2 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -4,7 +4,9 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -478,16 +480,15 @@ internal class Vp8Encoder : IDisposable WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } if (hasAlpha) @@ -501,7 +502,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } finally diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index fad6ca16cc..f0e4093194 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -99,7 +100,7 @@ internal class WebpAnimationDecoder : IDisposable remainingBytes -= 4; switch (chunkType) { - case WebpChunkType.Animation: + case WebpChunkType.FrameData: Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore ? new Color(new Bgra32(0, 0, 0, 0)) : features.AnimationBackgroundColor!.Value; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index f4e40090cf..80ffe8a996 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -106,14 +106,14 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new WebpImageInfo { @@ -139,7 +139,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -231,7 +231,7 @@ internal static class WebpChunkParsingUtils uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new WebpImageInfo + WebpImageInfo info = new() { Width = width, Height = height, @@ -247,7 +247,7 @@ internal static class WebpChunkParsingUtils /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -286,14 +286,14 @@ internal static class WebpChunkParsingUtils /// The stream to read the data from. /// Buffer to store the data read from the stream. /// The chunk size in bytes. - public static uint ReadChunkSize(BufferedReadStream stream, Span buffer) + public static uint ReadChunkSize(Stream stream, Span buffer) { - DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length"); + DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length"); - if (stream.Read(buffer) == 4) + if (stream.Read(buffer) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; + return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 5836dc6c09..12e3297775 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -61,5 +61,5 @@ internal enum WebpChunkType : uint /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// /// ANMF (Multiple) - Animation = 0x414E4D46, + FrameData = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 1433772757..818c843ea9 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -55,6 +55,11 @@ internal static class WebpConstants 0x50 // P }; + /// + /// The header bytes identifying a Webp. + /// + public const string WebpFourCc = "WEBP"; + /// /// 3 bits reserved for version. /// @@ -70,11 +75,6 @@ internal static class WebpConstants /// public const int Vp8FrameHeaderSize = 10; - /// - /// Size of a VP8X chunk in bytes. - /// - public const int Vp8XChunkSize = 10; - /// /// Size of a chunk header. /// diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs deleted file mode 100644 index 93c5d10dcd..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal struct WebpFrameData -{ - /// - /// The animation chunk size. - /// - public uint DataSize; - - /// - /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. - /// - public const uint HeaderSize = 16; - - /// - /// The X coordinate of the upper left corner of the frame is Frame X * 2. - /// - public uint X; - - /// - /// The Y coordinate of the upper left corner of the frame is Frame Y * 2. - /// - public uint Y; - - /// - /// The width of the frame. - /// - public uint Width; - - /// - /// The height of the frame. - /// - public uint Height; - - /// - /// The time to wait before displaying the next frame, in 1 millisecond units. - /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. - /// - public uint Duration; - - /// - /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. - /// - public WebpBlendingMethod BlendingMethod; - - /// - /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. - /// - public WebpDisposalMethod DisposalMethod; - - public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); - - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - public static WebpFrameData Parse(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - WebpFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; - - return data; - } -} From a477ac13bc358811cd4bb83c6ff0f19621f51ae5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:21:11 +1000 Subject: [PATCH 43/44] Use correct alpha blending --- .../Formats/Webp/WebpAnimationDecoder.cs | 64 +++++++++---------- .../Formats/WebP/WebpDecoderTests.cs | 10 +++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/landscape.webp | 3 + 4 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 tests/Images/Input/Webp/landscape.webp diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index f0e4093194..66e69d9a43 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -200,13 +200,10 @@ internal class WebpAnimationDecoder : IDisposable this.RestoreToBackground(imageFrame, backgroundColor); } - using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); + using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) - { - this.AlphaBlend(previousFrame, imageFrame, regionRectangle); - } + bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending; + DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); previousFrame = currentFrame ?? image.Frames.RootFrame; this.restoreArea = regionRectangle; @@ -253,7 +250,7 @@ internal class WebpAnimationDecoder : IDisposable /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); @@ -291,42 +288,43 @@ internal class WebpAnimationDecoder : IDisposable /// Draws the decoded image on canvas. The decoded image can be smaller the canvas. /// /// The type of the pixel. - /// The decoded image. + /// The decoded image. /// The image frame to draw into. /// The area of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) + /// Whether to blend the decoded frame data onto the target frame. + private static void DrawDecodedImageFrameOnCanvas( + Buffer2D decodedImageFrame, + ImageFrame imageFrame, + Rectangle restoreArea, + bool blend) where TPixel : unmanaged, IPixel { + // Trim the destination frame to match the restore area. The source frame is already trimmed. Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); - int decodedRowIdx = 0; - for (int y = 0; y < restoreArea.Height; y++) + if (blend) { - Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; - decodedPixelRow.TryCopyTo(framePixelRow); + // The destination frame has already been prepopulated with the pixel data from the previous frame + // so blending will leave the desired result which takes into consideration restoration to the + // background color within the restore area. + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + + for (int y = 0; y < restoreArea.Height; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + } + + return; } - } - /// - /// After disposing of the previous frame, render the current frame on the canvas using alpha-blending. - /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. - /// - /// The pixel format. - /// The source image. - /// The destination image. - /// The area of the frame. - 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 < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + decodedPixelRow.CopyTo(framePixelRow); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c3a777c153..4b03671e16 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -357,6 +357,16 @@ public class WebpDecoderTests image.CompareToOriginal(provider, ReferenceDecoder); } + [Theory] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_AlphaBlending_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + image.DebugSaveMultiFrame(provider); + image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact); + } + [Theory] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 048b19dc5b..6ad93adfbd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -681,6 +681,7 @@ public static class TestImages public static class Lossy { + public const string AnimatedLandscape = "Webp/landscape.webp"; public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; diff --git a/tests/Images/Input/Webp/landscape.webp b/tests/Images/Input/Webp/landscape.webp new file mode 100644 index 0000000000..5f1f31a055 --- /dev/null +++ b/tests/Images/Input/Webp/landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e9f8b7ee87ecb59d8cee5e84320da7670eb5e274e1c0a7dd5f13fe3675be62a +size 26892 From f46137847bed3d6c63f1b1f29bf8538aa7a384b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:42:20 +1000 Subject: [PATCH 44/44] Complete encoding tests --- .../Formats/WebP/WebpEncoderTests.cs | 32 +++++++++++++++---- ...Encode_AnimatedLossy_Rgba32_landscape.webp | 3 ++ ...imatedLossy_Rgba32_leo_animated_lossy.webp | 3 ++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d81c9eb93a..0ad684b277 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -24,22 +23,41 @@ public class WebpEncoderTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + image.VerifyEncoder(provider, "webp", string.Empty, encoder); } [Theory] [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] public void Encode_AnimatedLossy(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new()); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + // The reference decoder seems to produce differences up to 0.1% but the input/output have been + // checked to be correct. + string path = provider.Utility.GetTestOutputFileName("webp", null, true); + using Image encoded = Image.Load(path); + encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp"); } [Theory] diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp new file mode 100644 index 0000000000..2312cb8576 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ece3c7acc6f40318e3cda6b0189607df6b9b60dd112212c72ec0f6aa26431d +size 409346 diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp new file mode 100644 index 0000000000..8474504da7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71800dff476f50ebd2a3d0cf0b4f5bef427a1c2cd8732b415511f10d3d93f9a0 +size 126382