diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs
new file mode 100644
index 0000000000..8f06e5886f
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs
@@ -0,0 +1,124 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Common.Helpers;
+
+internal static class RiffHelper
+{
+ ///
+ /// The header bytes identifying RIFF file.
+ ///
+ private const 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.WriteUInt32BigEndian(buffer, fourCc);
+ stream.Write(buffer);
+
+ long sizePosition = stream.Position;
+ stream.Position += 4;
+
+ func(stream);
+
+ long position = stream.Position;
+
+ uint dataSize = (uint)(position - sizePosition - 4);
+
+ // padding
+ if (dataSize % 2 == 1)
+ {
+ stream.WriteByte(0);
+ position++;
+ }
+
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
+ stream.Position = sizePosition;
+ 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.WriteUInt32BigEndian(buffer, fourCc);
+ stream.Write(buffer);
+ uint size = (uint)data.Length;
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, size);
+ stream.Write(buffer);
+ stream.Write(data);
+
+ // padding
+ if (size % 2 is 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.WriteUInt32BigEndian(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;
+
+ uint dataSize = (uint)(position - sizePosition - 4);
+
+ // padding
+ if (dataSize % 2 is 1)
+ {
+ stream.WriteByte(0);
+ position++;
+ }
+
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
+ stream.Position = sizePosition;
+ stream.Write(buffer);
+ 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);
+}
diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
index f05f237576..1d743bf3a5 100644
--- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
+++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
@@ -123,12 +123,12 @@ 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/Chunks/AnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
new file mode 100644
index 0000000000..a9f99a9e4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
+
+internal readonly struct AnimationControl
+{
+ public const int Size = 8;
+
+ public AnimationControl(int numberFrames, int numberPlays)
+ {
+ this.NumberFrames = numberFrames;
+ this.NumberPlays = numberPlays;
+ }
+
+ ///
+ /// Gets the number of frames
+ ///
+ public int NumberFrames { get; }
+
+ ///
+ /// Gets the number of times to loop this APNG. 0 indicates infinite looping.
+ ///
+ public int NumberPlays { get; }
+
+ ///
+ /// Writes the acTL to the given buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
+ BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
+ }
+
+ ///
+ /// Parses the APngAnimationControl from the given data buffer.
+ ///
+ /// The data to parse.
+ /// The parsed acTL.
+ public static AnimationControl Parse(ReadOnlySpan data)
+ => new(
+ numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
+ numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
+}
diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
new file mode 100644
index 0000000000..fb2ca473c2
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
@@ -0,0 +1,160 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
+
+internal readonly struct FrameControl
+{
+ public const int Size = 26;
+
+ public FrameControl(uint width, uint height)
+ : this(0, width, height, 0, 0, 0, 0, default, default)
+ {
+ }
+
+ public FrameControl(
+ uint sequenceNumber,
+ uint width,
+ uint height,
+ uint xOffset,
+ uint yOffset,
+ ushort delayNumerator,
+ ushort delayDenominator,
+ PngDisposalMethod disposeOperation,
+ PngBlendMethod blendOperation)
+ {
+ this.SequenceNumber = sequenceNumber;
+ this.Width = width;
+ this.Height = height;
+ this.XOffset = xOffset;
+ this.YOffset = yOffset;
+ this.DelayNumerator = delayNumerator;
+ this.DelayDenominator = delayDenominator;
+ this.DisposeOperation = disposeOperation;
+ this.BlendOperation = blendOperation;
+ }
+
+ ///
+ /// Gets the sequence number of the animation chunk, starting from 0
+ ///
+ public uint SequenceNumber { get; }
+
+ ///
+ /// Gets the width of the following frame
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets the height of the following frame
+ ///
+ public uint Height { get; }
+
+ ///
+ /// Gets the X position at which to render the following frame
+ ///
+ public uint XOffset { get; }
+
+ ///
+ /// Gets the Y position at which to render the following frame
+ ///
+ public uint YOffset { get; }
+
+ ///
+ /// Gets the X limit at which to render the following frame
+ ///
+ public uint XMax => this.XOffset + this.Width;
+
+ ///
+ /// Gets the Y limit at which to render the following frame
+ ///
+ public uint YMax => this.YOffset + this.Height;
+
+ ///
+ /// Gets the frame delay fraction numerator
+ ///
+ public ushort DelayNumerator { get; }
+
+ ///
+ /// Gets the frame delay fraction denominator
+ ///
+ public ushort DelayDenominator { get; }
+
+ ///
+ /// Gets the type of frame area disposal to be done after rendering this frame
+ ///
+ public PngDisposalMethod DisposeOperation { get; }
+
+ ///
+ /// Gets the type of frame area rendering for this frame
+ ///
+ public PngBlendMethod BlendOperation { get; }
+
+ public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height);
+
+ ///
+ /// Validates the APng fcTL.
+ ///
+ /// The header.
+ ///
+ /// Thrown if the image does pass validation.
+ ///
+ public void Validate(PngHeader header)
+ {
+ if (this.Width == 0)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0");
+ }
+
+ if (this.Height == 0)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0");
+ }
+
+ if (this.XMax > header.Width)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
+ }
+
+ if (this.YMax > header.Height)
+ {
+ PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}");
+ }
+ }
+
+ ///
+ /// Writes the fcTL to the given buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.SequenceNumber);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[4..8], this.Width);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[8..12], this.Height);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], this.XOffset);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], this.YOffset);
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator);
+ BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator);
+
+ buffer[24] = (byte)this.DisposeOperation;
+ buffer[25] = (byte)this.BlendOperation;
+ }
+
+ ///
+ /// Parses the APngFrameControl from the given data buffer.
+ ///
+ /// The data to parse.
+ /// The parsed fcTL.
+ public static FrameControl Parse(ReadOnlySpan data)
+ => new(
+ sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
+ width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]),
+ height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]),
+ xOffset: BinaryPrimitives.ReadUInt32BigEndian(data[12..16]),
+ yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]),
+ delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]),
+ delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
+ disposeOperation: (PngDisposalMethod)data[24],
+ blendOperation: (PngBlendMethod)data[25]);
+}
diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
similarity index 98%
rename from src/ImageSharp/Formats/Png/PngHeader.cs
rename to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
index 06fec86f30..77fb706f60 100644
--- a/src/ImageSharp/Formats/Png/PngHeader.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
@@ -4,7 +4,7 @@
using System.Buffers.Binary;
-namespace SixLabors.ImageSharp.Formats.Png;
+namespace SixLabors.ImageSharp.Formats.Png.Chunks;
///
/// Represents the png header chunk.
diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
similarity index 89%
rename from src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs
rename to src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
index 34d53f00eb..7847882484 100644
--- a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
@@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
///
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
///
-internal readonly struct PhysicalChunkData
+internal readonly struct PngPhysical
{
public const int Size = 9;
- public PhysicalChunkData(uint x, uint y, byte unitSpecifier)
+ public PngPhysical(uint x, uint y, byte unitSpecifier)
{
this.XAxisPixelsPerUnit = x;
this.YAxisPixelsPerUnit = y;
@@ -44,13 +44,13 @@ 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..f24b8d1b5c 100644
--- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
@@ -14,7 +15,22 @@ public static partial class MetadataExtensions
///
/// Gets the png format specific metadata for the image.
///
- /// The metadata this method extends.
+ /// The metadata this method extends.
/// The .
- public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance);
+ public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
+
+ ///
+ /// Gets the aPng format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
+
+ ///
+ /// Gets the aPng format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The metadata.
+ /// The .
+ public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
}
diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs
new file mode 100644
index 0000000000..f71dce8325
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// Specifies whether the frame is to be alpha blended into the current output buffer content,
+/// or whether it should completely replace its region in the output buffer.
+///
+public enum PngBlendMethod
+{
+ ///
+ /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
+ ///
+ Source,
+
+ ///
+ /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as
+ /// described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
+ ///
+ Over
+}
diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs
index b514011eb3..e5fa5fbb72 100644
--- a/src/ImageSharp/Formats/Png/PngChunk.cs
+++ b/src/ImageSharp/Formats/Png/PngChunk.cs
@@ -42,7 +42,8 @@ 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..a008bf8ea2 100644
--- a/src/ImageSharp/Formats/Png/PngChunkType.cs
+++ b/src/ImageSharp/Formats/Png/PngChunkType.cs
@@ -9,15 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal enum PngChunkType : uint
{
///
- /// The IDAT chunk contains the actual image data. The image can contains more
+ /// This chunk contains the actual image data. The image can contains more
/// than one chunk of this type. All chunks together are the whole image.
///
+ /// IDAT (Multiple)
Data = 0x49444154U,
///
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
///
+ /// IEND (Single)
End = 0x49454E44U,
///
@@ -25,34 +27,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
///
+ /// IHDR (Single)
Header = 0x49484452U,
///
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
///
+ /// PLTE (Single)
Palette = 0x504C5445U,
///
/// The eXIf data chunk which contains the Exif profile.
///
+ /// eXIF (Single)
Exif = 0x65584966U,
///
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
///
+ /// gAMA (Single)
Gamma = 0x67414D41U,
///
- /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
+ /// This chunk specifies the intended pixel size or aspect ratio for display of the image.
///
+ /// pHYs (Single)
Physical = 0x70485973U,
///
/// Textual information that the encoder wishes to record with the image can be stored in
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
///
+ /// tEXT (Multiple)
Text = 0x74455874U,
///
@@ -60,70 +68,103 @@ internal enum PngChunkType : uint
/// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string.
///
+ /// zTXt (Multiple)
CompressedText = 0x7A545874U,
///
- /// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
+ /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
/// and the actual text string, which can be compressed or uncompressed.
///
+ /// iTXt (Multiple)
InternationalText = 0x69545874U,
///
- /// The tRNS chunk specifies that the image uses simple transparency:
+ /// This chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images).
///
+ /// tRNS (Single)
Transparency = 0x74524E53U,
///
- /// The tIME chunk gives the time of the last image modification (not the time of initial image creation).
+ /// This chunk gives the time of the last image modification (not the time of initial image creation).
///
+ /// tIME (Single)
Time = 0x74494d45,
///
- /// The bKGD chunk specifies a default background colour to present the image against.
+ /// This chunk specifies a default background colour to present the image against.
/// If there is any other preferred background, either user-specified or part of a larger page (as in a browser),
/// the bKGD chunk should be ignored.
///
+ /// bKGD (Single)
Background = 0x624b4744,
///
- /// The iCCP chunk contains a embedded color profile. If the iCCP chunk is present,
+ /// This chunk contains a embedded color profile. If the iCCP chunk is present,
/// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
///
+ /// iCCP (Single)
EmbeddedColorProfile = 0x69434350,
///
- /// The sBIT chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
+ /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
/// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
///
+ /// sBIT (Single)
SignificantBits = 0x73424954,
///
- /// If the sRGB chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
+ /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
/// using the specified rendering intent defined by the International Color Consortium.
///
+ /// sRGB (Single)
StandardRgbColourSpace = 0x73524742,
///
- /// The hIST chunk gives the approximate usage frequency of each colour in the palette.
+ /// This chunk gives the approximate usage frequency of each colour in the palette.
///
+ /// hIST (Single)
Histogram = 0x68495354,
///
- /// The sPLT chunk contains the suggested palette.
+ /// This chunk contains the suggested palette.
///
+ /// sPLT (Single)
SuggestedPalette = 0x73504c54,
///
- /// The cHRM chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
+ /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
/// green, and blue display primaries used in the image, and the referenced white point.
///
+ /// cHRM (Single)
Chroma = 0x6348524d,
+ ///
+ /// This chunk is an ancillary chunk as defined in the PNG Specification.
+ /// It must appear before the first IDAT chunk within a valid PNG stream.
+ ///
+ /// acTL (Single, APNG)
+ AnimationControl = 0x6163544cU,
+
+ ///
+ /// This chunk is an ancillary chunk as defined in the PNG Specification.
+ /// It must appear before the IDAT or fdAT chunks of the frame to which it applies.
+ ///
+ /// fcTL (Multiple, APNG)
+ FrameControl = 0x6663544cU,
+
+ ///
+ /// This chunk has the same purpose as an IDAT chunk.
+ /// It has the same structure as an IDAT chunk, except preceded by a sequence number.
+ ///
+ /// fdAT (Multiple, APNG)
+ FrameData = 0x66644154U,
+
///
/// Malformed chunk named CgBI produced by apple, which is not conform to the specification.
/// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410
///
+ /// CgBI
ProprietaryApple = 0x43674249
}
diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs
index b76c73b9f2..43f2b0fb25 100644
--- a/src/ImageSharp/Formats/Png/PngConstants.cs
+++ b/src/ImageSharp/Formats/Png/PngConstants.cs
@@ -28,12 +28,12 @@ internal static class PngConstants
///
/// The list of mimetypes that equate to a Png.
///
- public static readonly IEnumerable MimeTypes = new[] { "image/png" };
+ public static readonly IEnumerable MimeTypes = new[] { "image/png", "image/apng" };
///
/// The list of file extensions that equate to a Png.
///
- public static readonly IEnumerable FileExtensions = new[] { "png" };
+ public static readonly IEnumerable FileExtensions = new[] { "png", "apng" };
///
/// The header bytes as a big-endian coded ulong.
@@ -43,7 +43,7 @@ internal static class PngConstants
///
/// The dictionary of available color types.
///
- public static readonly Dictionary ColorTypes = new Dictionary
+ public static readonly Dictionary ColorTypes = new()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
@@ -80,7 +80,7 @@ internal static class PngConstants
///
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
///
- public static ReadOnlySpan XmpKeyword => new byte[]
+ public static ReadOnlySpan XmpKeyword => new[]
{
(byte)'X',
(byte)'M',
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 065d861e71..d8305a3f57 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -1,9 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using System.Buffers;
using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Compression;
using System.Runtime.CompilerServices;
@@ -34,12 +34,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
private readonly Configuration configuration;
///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
+ /// Whether the metadata should be ignored when the image is being decoded.
+ ///
+ private readonly uint maxFrames;
+
+ ///
+ /// Whether the metadata should be ignored when the image is being decoded.
///
private readonly bool skipMetadata;
///
- /// Gets or sets a value indicating whether to read the IHDR and tRNS chunks only.
+ /// Whether to read the IHDR and tRNS chunks only.
///
private readonly bool colorMetadataOnly;
@@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
/// The stream to decode from.
///
- private BufferedReadStream currentStream;
+ private BufferedReadStream currentStream = null!;
///
/// The png header.
///
private PngHeader header;
+ ///
+ /// The png animation control.
+ ///
+ private AnimationControl animationControl;
+
///
/// The number of bytes per pixel.
///
@@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
/// The palette containing color information for indexed png's.
///
- private byte[] palette;
+ private byte[] palette = null!;
///
/// The palette containing alpha channel color information for indexed png's.
///
- private byte[] paletteAlpha;
+ private byte[] paletteAlpha = null!;
///
/// Previous scanline processed.
///
- private IMemoryOwner previousScanline;
+ private IMemoryOwner previousScanline = null!;
///
/// The current scanline that is being processed.
///
- private IMemoryOwner scanline;
-
- ///
- /// The index of the current scanline being processed.
- ///
- private int currentRow = Adam7.FirstRow[0];
-
- ///
- /// The current number of bytes read in the current scanline.
- ///
- private int currentRowBytesRead;
+ private IMemoryOwner scanline = null!;
///
/// Gets or sets the png color type.
@@ -121,6 +121,7 @@ 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;
}
@@ -129,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;
@@ -144,11 +146,16 @@ 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;
this.currentStream.Skip(8);
- Image image = null;
+ Image? image = null;
+ FrameControl? previousFrameControl = null;
+ FrameControl? currentFrameControl = null;
+ ImageFrame? previousFrame = null;
+ ImageFrame? currentFrame = null;
Span buffer = stackalloc byte[20];
try
@@ -160,25 +167,84 @@ 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:
+ this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
+ break;
case PngChunkType.Physical:
ReadPhysicalChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Gamma:
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.FrameControl:
+ frameCount++;
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ currentFrame = null;
+ currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ if (image is null)
+ {
+ PngThrowHelper.ThrowMissingDefaultData();
+ }
+
+ if (currentFrameControl is null)
+ {
+ PngThrowHelper.ThrowMissingFrameControl();
+ }
+
+ previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
+ this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame);
+
+ this.currentStream.Position += 4;
+ this.ReadScanlines(
+ chunk.Length - 4,
+ currentFrame,
+ pngMetadata,
+ this.ReadNextDataChunkAndSkipSeq,
+ currentFrameControl.Value,
+ cancellationToken);
+
+ previousFrame = currentFrame;
+ previousFrameControl = currentFrameControl;
+ break;
case PngChunkType.Data:
+
+ currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
{
- this.InitializeImage(metadata, out image);
+ this.InitializeImage(metadata, currentFrameControl.Value, out image);
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
}
- this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken);
+ this.ReadScanlines(
+ chunk.Length,
+ image.Frames.RootFrame,
+ pngMetadata,
+ this.ReadNextDataChunk,
+ currentFrameControl.Value,
+ cancellationToken);
+ previousFrame = currentFrame;
+ previousFrameControl = currentFrameControl;
break;
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
@@ -245,9 +311,11 @@ 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;
+ FrameControl? lastFrameControl = null;
Span buffer = stackalloc byte[20];
this.currentStream.Skip(8);
@@ -263,6 +331,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngChunkType.Header:
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
+ case PngChunkType.AnimationControl:
+ this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan());
+ break;
case PngChunkType.Physical:
if (this.colorMetadataOnly)
{
@@ -281,8 +352,36 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadGammaChunk(pngMetadata, chunk.Data.GetSpan());
break;
- case PngChunkType.Data:
+ case PngChunkType.FrameControl:
+ ++frameCount;
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ break;
+ case PngChunkType.FrameData:
+ if (frameCount == this.maxFrames)
+ {
+ break;
+ }
+ if (this.colorMetadataOnly)
+ {
+ goto EOF;
+ }
+
+ if (lastFrameControl is null)
+ {
+ PngThrowHelper.ThrowMissingFrameControl();
+ }
+
+ // Skip sequence number
+ this.currentStream.Skip(4);
+ this.SkipChunkDataAndCrc(chunk);
+ break;
+ case PngChunkType.Data:
// Spec says tRNS must be before IDAT so safe to exit.
if (this.colorMetadataOnly)
{
@@ -369,7 +468,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
EOF:
if (this.header.Width == 0 && this.header.Height == 0)
{
- PngThrowHelper.ThrowNoHeader();
+ PngThrowHelper.ThrowInvalidHeader();
}
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
@@ -403,7 +502,7 @@ 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)
{
@@ -438,7 +537,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
@@ -471,8 +570,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, FrameControl frameControl, out Image image)
where TPixel : unmanaged, IPixel
{
image = Image.CreateUninitialized(
@@ -481,6 +581,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header.Height,
metadata);
+ PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata();
+ frameMetadata.FromChunk(in frameControl);
+
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
@@ -495,6 +598,47 @@ 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 previous frame.
+ /// The frame control information for the current frame.
+ /// The image that we will populate
+ /// The previous frame.
+ /// The created frame
+ private void InitializeFrame(
+ FrameControl previousFrameControl,
+ FrameControl currentFrameControl,
+ Image image,
+ ImageFrame? previousFrame,
+ out ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ // We create a clone of the previous frame and add it.
+ // We will overpaint the difference of pixels on the current frame to create a complete image.
+ // This ensures that we have enough pixel data to process without distortion. #2450
+ frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame);
+
+ // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
+ if (previousFrameControl.DisposeOperation == PngDisposalMethod.Background
+ || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.Previous))
+ {
+ Rectangle restoreArea = previousFrameControl.Bounds;
+ Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
+ Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
+ pixelRegion.Clear();
+ }
+
+ PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata();
+ frameMetadata.FromChunk(currentFrameControl);
+
+ this.previousScanline?.Dispose();
+ this.scanline?.Dispose();
+ this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
+ this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
+ }
+
///
/// Calculates the correct number of bits per pixel for the given color type.
///
@@ -558,24 +702,32 @@ 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 frame control
/// The cancellation token.
- private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void ReadScanlines(
+ int chunkLength,
+ ImageFrame image,
+ PngMetadata pngMetadata,
+ Func getData,
+ in FrameControl frameControl,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk);
- deframeStream.AllocateNewBytes(chunk.Length, true);
- DeflateStream dataStream = deframeStream.CompressedStream;
+ using ZlibInflateStream inflateStream = new(this.currentStream, getData);
+ inflateStream.AllocateNewBytes(chunkLength, true);
+ DeflateStream dataStream = inflateStream.CompressedStream!;
- if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
+ if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{
- this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken);
+ this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
else
{
- this.DecodePixelData(dataStream, image, pngMetadata, cancellationToken);
+ this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
}
@@ -583,29 +735,48 @@ 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 image frame to decode to.
/// The png metadata
/// The CancellationToken
- private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void DecodePixelData(
+ FrameControl frameControl,
+ DeflateStream compressedStream,
+ ImageFrame imageFrame,
+ PngMetadata pngMetadata,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- while (this.currentRow < this.header.Height)
+ int currentRow = (int)frameControl.YOffset;
+ int currentRowBytesRead = 0;
+ int height = (int)frameControl.YMax;
+
+ IMemoryOwner? blendMemory = null;
+ Span blendRowBuffer = Span.Empty;
+ if (frameControl.BlendOperation == PngBlendMethod.Over)
+ {
+ blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean);
+ blendRowBuffer = blendMemory.Memory.Span;
+ }
+
+ while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
- Span scanlineSpan = this.scanline.GetSpan();
- while (this.currentRowBytesRead < this.bytesPerScanline)
+ int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1;
+ Span scanlineSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
+ while (currentRowBytesRead < bytesPerFrameScanline)
{
- int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
+ int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
}
- this.currentRowBytesRead += bytesRead;
+ currentRowBytesRead += bytesRead;
}
- this.currentRowBytesRead = 0;
+ currentRowBytesRead = 0;
switch ((FilterType)scanlineSpan[0])
{
@@ -633,28 +804,47 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata);
-
+ this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata, blendRowBuffer);
this.SwapScanlineBuffers();
- this.currentRow++;
+ currentRow++;
}
+
+ blendMemory?.Dispose();
}
///
/// Decodes the raw interlaced pixel data row by row
- ///
///
/// The pixel format.
+ /// The frame control
/// The compressed pixel data stream.
- /// The current image.
+ /// The current image frame.
/// The png metadata.
/// The cancellation token.
- private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken)
+ private void DecodeInterlacedPixelData(
+ in FrameControl frameControl,
+ DeflateStream compressedStream,
+ ImageFrame imageFrame,
+ PngMetadata pngMetadata,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset;
+ int currentRowBytesRead = 0;
int pass = 0;
- int width = this.header.Width;
- Buffer2D imageBuffer = image.PixelBuffer;
+ int width = (int)frameControl.Width;
+ int endRow = (int)frameControl.YMax;
+
+ Buffer2D imageBuffer = imageFrame.PixelBuffer;
+
+ IMemoryOwner? blendMemory = null;
+ Span blendRowBuffer = Span.Empty;
+ if (frameControl.BlendOperation == PngBlendMethod.Over)
+ {
+ blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean);
+ blendRowBuffer = blendMemory.Memory.Span;
+ }
+
while (true)
{
int numColumns = Adam7.ComputeColumns(width, pass);
@@ -669,21 +859,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
- while (this.currentRow < this.header.Height)
+ while (currentRow < endRow)
{
cancellationToken.ThrowIfCancellationRequested();
- while (this.currentRowBytesRead < bytesPerInterlaceScanline)
+ while (currentRowBytesRead < bytesPerInterlaceScanline)
{
- int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead);
+ int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
}
- this.currentRowBytesRead += bytesRead;
+ currentRowBytesRead += bytesRead;
}
- this.currentRowBytesRead = 0;
+ currentRowBytesRead = 0;
Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline);
Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline);
@@ -714,12 +904,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow);
- this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
-
+ Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
+ this.ProcessInterlacedDefilteredScanline(
+ frameControl,
+ this.scanline.GetSpan(),
+ rowSpan,
+ pngMetadata,
+ blendRowBuffer,
+ pixelOffset: Adam7.FirstColumn[pass],
+ increment: Adam7.ColumnIncrement[pass]);
+
+ blendRowBuffer.Clear();
this.SwapScanlineBuffers();
- this.currentRow += Adam7.RowIncrement[pass];
+ currentRow += Adam7.RowIncrement[pass];
}
pass++;
@@ -727,7 +925,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (pass < 7)
{
- this.currentRow = Adam7.FirstRow[pass];
+ currentRow = Adam7.FirstRow[pass];
}
else
{
@@ -735,27 +933,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
}
+
+ blendMemory?.Dispose();
}
///
/// Processes the de-filtered scanline filling the image pixel data
///
/// The pixel format.
- /// The de-filtered scanline
+ /// The frame control
+ /// The index of the current scanline being processed.
+ /// The de-filtered scanline
/// The image
/// The png metadata.
- private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata)
+ /// A span used to temporarily hold the decoded row pixel data for alpha blending.
+ private void ProcessDefilteredScanline(
+ in FrameControl frameControl,
+ int currentRow,
+ ReadOnlySpan scanline,
+ ImageFrame pixels,
+ PngMetadata pngMetadata,
+ Span blendRowBuffer)
where TPixel : unmanaged, IPixel
{
- Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow);
+ Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
+
+ bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
+ Span rowSpan = blend
+ ? blendRowBuffer
+ : destination;
// Trim the first marker byte from the buffer
- ReadOnlySpan trimmed = defilteredScanline[1..];
+ ReadOnlySpan trimmed = scanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- IMemoryOwner buffer = null;
+ IMemoryOwner? buffer = null;
try
{
+ // TODO: The allocation here could be per frame, not per scanline.
ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
trimmed,
this.bytesPerScanline - 1,
@@ -768,7 +983,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessGrayscaleScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
pngMetadata.TransparentColor);
@@ -777,7 +993,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)this.bytesPerPixel,
@@ -787,7 +1004,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.Palette:
PngScanlineProcessor.ProcessPaletteScanline(
- this.header,
+ in frameControl,
scanlineSpan,
rowSpan,
pngMetadata.ColorTable);
@@ -797,7 +1014,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.Rgb:
PngScanlineProcessor.ProcessRgbScanline(
this.configuration,
- this.header,
+ this.header.BitDepth,
+ frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
@@ -809,7 +1027,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessRgbaScanline(
this.configuration,
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
@@ -817,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
{
@@ -828,19 +1054,33 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Processes the interlaced de-filtered scanline filling the image pixel data
///
/// The pixel format.
- /// The de-filtered scanline
- /// The current image row.
+ /// The frame control
+ /// The de-filtered scanline
+ /// The current image row.
/// The png metadata.
+ /// A span used to temporarily hold the decoded row pixel data for alpha blending.
/// The column start index. Always 0 for none interlaced images.
/// The column increment. Always 1 for none interlaced images.
- private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1)
+ private void ProcessInterlacedDefilteredScanline(
+ in FrameControl frameControl,
+ ReadOnlySpan scanline,
+ Span destination,
+ PngMetadata pngMetadata,
+ Span blendRowBuffer,
+ int pixelOffset = 0,
+ int increment = 1)
where TPixel : unmanaged, IPixel
{
+ bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
+ Span rowSpan = blend
+ ? blendRowBuffer
+ : destination;
+
// Trim the first marker byte from the buffer
- ReadOnlySpan trimmed = defilteredScanline[1..];
+ ReadOnlySpan trimmed = scanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
- IMemoryOwner buffer = null;
+ IMemoryOwner? buffer = null;
try
{
ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(
@@ -855,7 +1095,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -866,7 +1107,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
- this.header,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -878,7 +1120,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.Palette:
PngScanlineProcessor.ProcessInterlacedPaletteScanline(
- this.header,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -889,7 +1131,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.Rgb:
PngScanlineProcessor.ProcessInterlacedRgbScanline(
- this.header,
+ this.configuration,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -902,7 +1146,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessInterlacedRgbaScanline(
- this.header,
+ this.configuration,
+ this.header.BitDepth,
+ in frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
@@ -912,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
{
@@ -996,6 +1249,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 = AnimationControl.Parse(data);
+
+ pngMetadata.RepeatCount = this.animationControl.NumberPlays;
+ }
+
+ ///
+ /// Reads a header chunk from the data.
+ ///
+ /// The containing data.
+ private FrameControl ReadFrameControlChunk(ReadOnlySpan data)
+ {
+ FrameControl fcTL = FrameControl.Parse(data);
+
+ fcTL.Validate(this.header);
+
+ return fcTL;
+ }
+
///
/// Reads a header chunk from the data.
///
@@ -1083,7 +1361,7 @@ 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));
@@ -1376,7 +1654,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));
}
@@ -1399,7 +1677,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))
{
@@ -1424,11 +1702,11 @@ 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))
{
- if (chunk.Type == PngChunkType.Data)
+ if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData)
{
chunk.Data?.Dispose();
return chunk.Length;
@@ -1440,6 +1718,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.
///
@@ -1497,9 +1791,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/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
new file mode 100644
index 0000000000..17391de95c
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
+///
+public enum PngDisposalMethod
+{
+ ///
+ /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
+ ///
+ None,
+
+ ///
+ /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
+ ///
+ Background,
+
+ ///
+ /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
+ ///
+ Previous
+}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index c16348b8d9..04e3b1d840 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using System.Buffers;
using System.Buffers.Binary;
@@ -100,18 +99,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
/// The raw data of previous scanline.
///
- private IMemoryOwner previousScanline;
+ private IMemoryOwner previousScanline = null!;
///
/// The raw data of current scanline.
///
- private IMemoryOwner currentScanline;
+ private IMemoryOwner currentScanline = null!;
///
/// The color profile name.
///
private const string ColorProfileName = "ICC Profile";
+ ///
+ /// The encoder quantizer, if present.
+ ///
+ private IQuantizer? quantizer;
+
///
/// Initializes a new instance of the class.
///
@@ -122,6 +126,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
+ this.quantizer = encoder.Quantizer;
}
///
@@ -141,20 +146,23 @@ 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;
- bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
+
+ stream.Write(PngConstants.HeaderBytes);
+
+ ImageFrame? clonedFrame = null;
+ ImageFrame currentFrame = image.Frames.RootFrame;
+
+ bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
{
- clonedImage = image.Clone();
- ClearTransparentPixels(clonedImage);
+ currentFrame = clonedFrame = currentFrame.Clone();
+ ClearTransparentPixels(currentFrame);
}
- IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
-
- stream.Write(PngConstants.HeaderBytes);
+ // Do not move this. We require an accurate bit depth for the header chunk.
+ IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
@@ -165,13 +173,58 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
- this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
+
+ if (image.Frames.Count > 1)
+ {
+ this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.RepeatCount);
+
+ // TODO: We should attempt to optimize the output by clipping the indexed result to
+ // non-transparent bounds. That way we can assign frame control bounds and encode
+ // less data. See GifEncoder for the implementation there.
+
+ // Write the first frame.
+ FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0);
+ this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+
+ // Capture the global palette for reuse on subsequent frames.
+ ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
+
+ // Write following frames.
+ uint increment = 0;
+ for (int i = 1; i < image.Frames.Count; i++)
+ {
+ currentFrame = image.Frames[i];
+ if (clearTransparency)
+ {
+ // Dispose of previous clone and reassign.
+ clonedFrame?.Dispose();
+ currentFrame = clonedFrame = currentFrame.Clone();
+ ClearTransparentPixels(currentFrame);
+ }
+
+ // Each frame control sequence number must be incremented by the
+ // number of frame data chunks that follow.
+ frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment);
+
+ // Dispose of previous quantized frame and reassign.
+ quantized?.Dispose();
+ quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette);
+ increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true);
+ }
+ }
+ else
+ {
+ FrameControl frameControl = new((uint)this.width, (uint)this.height);
+ this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+ }
+
this.WriteEndChunk(stream);
stream.Flush();
+ // Dispose of allocations from final frame.
+ clonedFrame?.Dispose();
quantized?.Dispose();
- clonedImage?.Dispose();
}
///
@@ -179,18 +232,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
- this.previousScanline = null;
- this.currentScanline = null;
}
///
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
///
/// The type of the pixel.
- /// The cloned image where the transparent pixels will be changed.
- private static void ClearTransparentPixels(Image image)
- where TPixel : unmanaged, IPixel =>
- image.ProcessPixelRows(accessor =>
+ /// The cloned image frame where the transparent pixels will be changed.
+ private static void ClearTransparentPixels(ImageFrame clone)
+ where TPixel : unmanaged, IPixel
+ => clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 rgba32 = default;
@@ -202,7 +253,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
span[x].ToRgba32(ref rgba32);
- if (rgba32.A == 0)
+ if (rgba32.A is 0)
{
span[x].FromRgba32(transparent);
}
@@ -214,24 +265,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Creates the quantized image and calculates and sets the bit depth.
///
/// The type of the pixel.
- /// The image to quantize.
- /// Cloned image with transparent pixels are changed to black.
+ /// The image metadata.
+ /// The frame to quantize.
+ /// Any previously derived palette.
/// The quantized image.
- private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth(
- Image image,
- Image clonedImage)
+ private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth(
+ PngMetadata metadata,
+ ImageFrame frame,
+ ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
- IndexedImageFrame quantized;
- if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
- }
- else
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
- }
-
+ IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
@@ -242,9 +286,7 @@ 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)
{
@@ -400,20 +442,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;
@@ -474,7 +515,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
{
@@ -574,6 +615,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)
+ {
+ AnimationControl acTL = new(framesCount, playsCount);
+
+ acTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size);
+ }
+
///
/// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk.
@@ -581,7 +637,7 @@ 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)
@@ -640,14 +696,14 @@ 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;
}
- PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
+ PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
- this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size);
+ this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size);
}
///
@@ -689,9 +745,9 @@ 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;
}
@@ -758,18 +814,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))
{
@@ -932,14 +979,45 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}
}
+ ///
+ /// Writes the animation control chunk to the stream.
+ ///
+ /// The containing image data.
+ /// The image frame.
+ /// The frame sequence number.
+ private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
+ {
+ PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
+
+ // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
+ FrameControl fcTL = new(
+ sequenceNumber: sequenceNumber,
+ width: (uint)imageFrame.Width,
+ height: (uint)imageFrame.Height,
+ xOffset: 0,
+ yOffset: 0,
+ delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
+ delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
+ disposeOperation: frameMetadata.DisposalMethod,
+ blendOperation: frameMetadata.BlendMethod);
+
+ fcTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size);
+
+ return fcTL;
+ }
+
///
/// Writes the pixel information to the stream.
///
/// The pixel format.
- /// The image.
+ /// The frame control
+ /// The frame.
/// The quantized pixel data. Can be null.
/// The stream.
- private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream)
+ /// Is writing fdAT or IDAT.
+ private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -949,20 +1027,20 @@ 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);
+ 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);
}
}
@@ -972,24 +1050,42 @@ 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++;
}
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)
+ {
+ // We increment the sequence number for each frame chunk.
+ // '1' is added to the sequence number to account for the preceding frame control chunk.
+ uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i);
+ this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length);
+ }
+ else
+ {
+ this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length);
+ }
}
+
+ return (uint)numChunks;
}
///
@@ -1009,13 +1105,17 @@ 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(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int bytesPerScanline = this.CalculateScanlineLength(this.width);
+ int width = (int)frameControl.Width;
+ int height = (int)frameControl.Height;
+
+ int bytesPerScanline = this.CalculateScanlineLength(width);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
@@ -1026,7 +1126,7 @@ 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 = (int)frameControl.YOffset; y < frameControl.YMax; y++)
{
this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter);
@@ -1039,18 +1139,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the pixels.
///
/// The type of the pixel.
- /// The image.
+ /// The frame control
+ /// The image frame.
/// The deflate stream.
- private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
- Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer;
+ int width = (int)frameControl.XMax;
+ int height = (int)frameControl.YMax;
+ Buffer2D pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass];
- int startCol = Adam7.FirstColumn[pass];
+ int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
+ int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
@@ -1072,7 +1173,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
// Collect pixel data
Span srcRow = pixelBuffer.DangerousGetRowSpan(row);
- for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass])
+ for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass])
{
block[i++] = srcRow[col];
}
@@ -1092,17 +1193,18 @@ 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 = (int)frameControl.Width;
+ int endRow = (int)frameControl.YMax;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass];
- int startCol = Adam7.FirstColumn[pass];
+ int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
+ int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
@@ -1121,17 +1223,16 @@ 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 < endRow; row += Adam7.RowIncrement[pass])
{
// Collect data
ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row);
for (int col = startCol, i = 0;
- col < width;
+ col < frameControl.XMax;
col += Adam7.ColumnIncrement[pass])
{
- block[i++] = srcRow[col];
+ block[i] = srcRow[col];
+ i++;
}
// Encode data
@@ -1163,7 +1264,7 @@ 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)
@@ -1189,6 +1290,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.
///
@@ -1198,7 +1331,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;
@@ -1242,14 +1375,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.
@@ -1265,7 +1391,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;
}
@@ -1276,40 +1402,50 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The png encoder.
/// The color type.
/// The bits per component.
- /// The image.
- private static IndexedImageFrame CreateQuantizedFrame(
+ /// The image metadata.
+ /// The frame to quantize.
+ /// Any previously derived palette.
+ private IndexedImageFrame? CreateQuantizedFrame(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
- Image image)
+ PngMetadata metadata,
+ ImageFrame frame,
+ ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
- if (colorType != PngColorType.Palette)
+ if (colorType is not PngColorType.Palette)
{
return null;
}
+ if (previousPalette is not null)
+ {
+ // Use the previously derived palette created by quantizing the root frame to quantize the current frame.
+ using PaletteQuantizer paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1);
+ paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
+ return paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
+ }
+
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
- IQuantizer quantizer = encoder.Quantizer;
- if (quantizer is null)
+ if (this.quantizer is null)
{
- PngMetadata metadata = image.Metadata.GetPngMetadata();
if (metadata.ColorTable is not null)
{
- // Use the provided palette in total. The caller is responsible for setting values.
- quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
+ // Use the provided palette. The caller is responsible for setting values.
+ this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
}
else
{
- quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
+ this.quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
}
}
// Create quantized frame returning the palette and set the bit depth.
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.Configuration);
+ using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration);
- frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image);
- return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
+ return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
///
@@ -1323,25 +1459,23 @@ 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;
}
@@ -1379,21 +1513,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
};
@@ -1404,27 +1538,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/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs
index 2d1f2dcc7d..e5852affa9 100644
--- a/src/ImageSharp/Formats/Png/PngFormat.cs
+++ b/src/ImageSharp/Formats/Png/PngFormat.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
/// Registers the image encoders, decoders and mime type detectors for the png format.
///
-public sealed class PngFormat : IImageFormat
+public sealed class PngFormat : IImageFormat
{
private PngFormat()
{
@@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat
///
public PngMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
new file mode 100644
index 0000000000..ca4d8c1f45
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Png.Chunks;
+
+namespace SixLabors.ImageSharp.Formats.Png;
+
+///
+/// Provides APng specific metadata information for the image frame.
+///
+public class PngFrameMetadata : IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PngFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ private PngFrameMetadata(PngFrameMetadata other)
+ {
+ this.FrameDelay = other.FrameDelay;
+ this.DisposalMethod = other.DisposalMethod;
+ this.BlendMethod = other.BlendMethod;
+ }
+
+ ///
+ /// Gets or sets the frame delay for animated images.
+ /// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to
+ /// wait before continuing with the processing of the Data Stream.
+ /// The clock starts ticking immediately after the graphic is rendered.
+ ///
+ public Rational FrameDelay { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area disposal to be done after rendering this frame
+ ///
+ public PngDisposalMethod DisposalMethod { get; set; }
+
+ ///
+ /// Gets or sets the type of frame area rendering for this frame
+ ///
+ public PngBlendMethod BlendMethod { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chunk to create an instance from.
+ internal void FromChunk(in FrameControl frameControl)
+ {
+ this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
+ this.DisposalMethod = frameControl.DisposeOperation;
+ this.BlendMethod = frameControl.BlendOperation;
+ }
+
+ ///
+ public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
+}
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 8806c29b1a..b113dbfc17 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Png.Chunks;
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats.Png;
///
@@ -26,6 +29,7 @@ public class PngMetadata : IDeepCloneable
this.Gamma = other.Gamma;
this.InterlaceMethod = other.InterlaceMethod;
this.TransparentColor = other.TransparentColor;
+ this.RepeatCount = other.RepeatCount;
if (other.ColorTable?.Length > 0)
{
@@ -75,6 +79,11 @@ 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 RepeatCount { get; set; }
+
///
public IDeepCloneable DeepClone() => new PngMetadata(this);
}
diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
index b0afd9975e..f217515e3c 100644
--- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
+++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
@@ -4,6 +4,7 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -15,75 +16,24 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal static class PngScanlineProcessor
{
public static void ProcessGrayscaleScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
Color? transparentColor)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
-
- if (transparentColor is null)
- {
- if (header.BitDepth == 16)
- {
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
- {
- ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- pixel.FromL16(Unsafe.As(ref luminance));
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
- pixel.FromL8(Unsafe.As(ref luminance));
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
-
- return;
- }
-
- if (header.BitDepth == 16)
- {
- L16 transparent = transparentColor.Value.ToPixel();
- La32 source = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
- {
- ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- source.L = luminance;
- source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue;
-
- pixel.FromLa32(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
- La16 source = default;
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
- source.L = luminance;
- source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
-
- pixel.FromLa16(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedGrayscaleScanline(
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ transparentColor);
public static void ProcessInterlacedGrayscaleScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -91,17 +41,18 @@ internal static class PngScanlineProcessor
Color? transparentColor)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
+ int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1);
if (transparentColor is null)
{
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
pixel.FromL16(Unsafe.As(ref luminance));
@@ -110,7 +61,7 @@ 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.XMax; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
pixel.FromL8(Unsafe.As(ref luminance));
@@ -121,12 +72,12 @@ internal static class PngScanlineProcessor
return;
}
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
L16 transparent = transparentColor.Value.ToPixel();
La32 source = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance;
@@ -140,7 +91,7 @@ internal static class PngScanlineProcessor
{
byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
La16 source = default;
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
+ for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
source.L = luminance;
@@ -153,47 +104,26 @@ internal static class PngScanlineProcessor
}
public static void ProcessGrayscaleWithAlphaScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint bytesPerPixel,
uint bytesPerSample)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (header.BitDepth == 16)
- {
- La32 source = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += 4)
- {
- source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
- source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
-
- pixel.FromLa32(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- La16 source = default;
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- nuint offset = x * bytesPerPixel;
- source.L = Unsafe.Add(ref scanlineSpanRef, offset);
- source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
-
- pixel.FromLa16(source);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedGrayscaleWithAlphaScanline(
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ bytesPerPixel,
+ bytesPerSample);
public static void ProcessInterlacedGrayscaleWithAlphaScanline(
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -202,15 +132,16 @@ internal static class PngScanlineProcessor
uint bytesPerSample)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
La32 source = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 4)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += 4)
{
source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
@@ -222,46 +153,35 @@ 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.XMax; x += increment)
{
- source.L = Unsafe.Add(ref scanlineSpanRef, offset);
- source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
+ source.L = Unsafe.Add(ref scanlineSpanRef, offset2);
+ source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample);
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
- offset += bytesPerPixel;
+ offset2 += bytesPerPixel;
}
}
}
public static void ProcessPaletteScanline(
- in PngHeader header,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
ReadOnlyMemory? palette)
- where TPixel : unmanaged, IPixel
- {
- if (palette is null)
- {
- PngThrowHelper.ThrowMissingPalette();
- }
-
- TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
-
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- uint index = Unsafe.Add(ref scanlineSpanRef, x);
- pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedPaletteScanline(
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ palette);
public static void ProcessInterlacedPaletteScanline(
- in PngHeader header,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -279,7 +199,7 @@ internal static class PngScanlineProcessor
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
+ for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++)
{
uint index = Unsafe.Add(ref scanlineSpanRef, o);
pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
@@ -289,82 +209,30 @@ internal static class PngScanlineProcessor
public static void ProcessRgbScanline(
Configuration configuration,
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
int bytesPerPixel,
int bytesPerSample,
Color? transparentColor)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (transparentColor is null)
- {
- if (header.BitDepth == 16)
- {
- Rgb48 rgb48 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
-
- pixel.FromRgb48(rgb48);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width);
- }
-
- return;
- }
-
- if (header.BitDepth == 16)
- {
- Rgb48 transparent = transparentColor.Value.ToPixel();
-
- Rgb48 rgb48 = default;
- Rgba64 rgba64 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
-
- rgba64.Rgb = rgb48;
- rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
-
- pixel.FromRgba64(rgba64);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- Rgb24 transparent = transparentColor.Value.ToPixel();
-
- Rgba32 rgba32 = default;
- ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan);
- ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
- rgba32.Rgb = rgb24;
- rgba32.A = rgb24.Equals(transparent) ? byte.MinValue : byte.MaxValue;
-
- pixel.FromRgba32(rgba32);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedRgbScanline(
+ configuration,
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ bytesPerPixel,
+ bytesPerSample,
+ transparentColor);
public static void ProcessInterlacedRgbScanline(
- in PngHeader header,
+ Configuration configuration,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -374,18 +242,19 @@ internal static class PngScanlineProcessor
Color? transparentColor)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
+
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- bool hasTransparency = transparentColor is not null;
if (transparentColor is null)
{
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
Rgb48 rgb48 = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@@ -395,11 +264,19 @@ 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
{
Rgb24 rgb = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
@@ -413,14 +290,14 @@ internal static class PngScanlineProcessor
return;
}
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
Rgb48 transparent = transparentColor.Value.ToPixel();
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@@ -439,7 +316,7 @@ 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.XMax; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
@@ -454,39 +331,28 @@ internal static class PngScanlineProcessor
public static void ProcessRgbaScanline(
Configuration configuration,
- in PngHeader header,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
int bytesPerPixel,
int bytesPerSample)
- where TPixel : unmanaged, IPixel
- {
- TPixel pixel = default;
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
-
- if (header.BitDepth == 16)
- {
- Rgba64 rgba64 = default;
- int o = 0;
- for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel)
- {
- rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
- rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
- rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
- rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
-
- pixel.FromRgba64(rgba64);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width);
- }
- }
+ where TPixel : unmanaged, IPixel =>
+ ProcessInterlacedRgbaScanline(
+ configuration,
+ bitDepth,
+ frameControl,
+ scanlineSpan,
+ rowSpan,
+ 0,
+ 1,
+ bytesPerPixel,
+ bytesPerSample);
public static void ProcessInterlacedRgbaScanline(
- in PngHeader header,
+ Configuration configuration,
+ int bitDepth,
+ in FrameControl frameControl,
ReadOnlySpan scanlineSpan,
Span rowSpan,
uint pixelOffset,
@@ -495,15 +361,15 @@ internal static class PngScanlineProcessor
int bytesPerSample)
where TPixel : unmanaged, IPixel
{
+ uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default;
- ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- if (header.BitDepth == 16)
+ if (bitDepth == 16)
{
Rgba64 rgba64 = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@@ -514,11 +380,20 @@ 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
{
+ ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
Rgba32 rgba = default;
int o = 0;
- for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
+ for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
index 67da78e45b..0552e9a79e 100644
--- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs
+++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -12,13 +13,22 @@ 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 +40,15 @@ 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 ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "")
+ => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'.");
+
+ [DoesNotReturn]
+ public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value1))] string name2 = "")
+ => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'.");
+
+ [DoesNotReturn]
+ public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type.");
[DoesNotReturn]
public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type.");
diff --git a/src/ImageSharp/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 596715b205..cbd2aa8e7f 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,19 +73,19 @@ 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 ImageFrame(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++)
@@ -95,23 +95,23 @@ internal static class AlphaEncoder
}
}
- 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
deleted file mode 100644
index 714ec428ec..0000000000
--- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats.Webp;
-
-internal struct AnimationFrameData
-{
- ///
- /// The animation chunk size.
- ///
- public uint DataSize;
-
- ///
- /// 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 AnimationBlendingMethod 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;
-}
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index ab78d18604..d502fd6063 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -1,9 +1,11 @@
// 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;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@@ -14,18 +16,11 @@ internal abstract class BitWriterBase
private const ulong MaxCanvasPixels = 4294967295ul;
- protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
-
///
/// Buffer to write to.
///
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.
///
@@ -41,17 +36,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 +60,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.
///
@@ -84,63 +79,89 @@ internal abstract class BitWriterBase
}
///
- /// Writes the RIFF header to the stream.
+ /// Write the trunks before data trunk.
///
/// The stream to write to.
- /// The block length.
- protected void WriteRiffHeader(Stream stream, uint riffSize)
+ /// 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.
+ /// Flag indicating, if an animation parameter is present.
+ public static void WriteTrunksBeforeData(
+ Stream stream,
+ uint width,
+ uint height,
+ ExifProfile? exifProfile,
+ XmpProfile? xmpProfile,
+ IccProfile? iccProfile,
+ bool hasAlpha,
+ bool hasAnimation)
{
- stream.Write(WebpConstants.RiffFourCc);
- BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
- stream.Write(this.scratchBuffer.Span.Slice(0, 4));
- stream.Write(WebpConstants.WebpHeader);
+ // Write file size later
+ 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;
+ if (isVp8X)
+ {
+ WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
+
+ if (iccProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
+ }
+ }
}
///
- /// Calculates the chunk size of EXIF, XMP or ICCP metadata.
+ /// Writes the encoded image to the stream.
///
- /// 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);
- }
+ /// The stream to write to.
+ public abstract void WriteEncodedImageToStream(Stream stream);
///
- /// Calculates the chunk size of a alpha chunk.
+ /// Write the trunks after data trunk.
///
- /// The alpha chunk bytes.
- /// The alpha data chunk size in bytes.
- protected static uint AlphaChunkSize(Span alphaBytes)
+ /// The stream to write to.
+ /// The exif profile.
+ /// The XMP profile.
+ public static void WriteTrunksAfterData(
+ Stream stream,
+ ExifProfile? exifProfile,
+ XmpProfile? xmpProfile)
{
- uint alphaSize = (uint)alphaBytes.Length + 1;
- return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
+ if (exifProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
+ }
+
+ if (xmpProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
+ }
+
+ RiffHelper.EndWriteRiffFile(stream, 4);
}
///
- /// Writes a metadata profile (EXIF or XMP) to the stream.
+ /// Writes the animation parameter() to the stream.
///
/// The stream to write to.
- /// The metadata profile's bytes.
- /// The chuck type to write.
- protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
+ ///
+ /// 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.
+ public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount)
{
- DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
-
- uint size = (uint)metadataBytes.Length;
- Span buf = this.scratchBuffer.Span.Slice(0, 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);
- }
+ WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
+ chunk.WriteTo(stream);
}
///
@@ -149,53 +170,19 @@ internal abstract class BitWriterBase
/// 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.Slice(0, 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)
{
- flags |= 1;
+ // TODO: Filtering and preprocessing
+ flags = 1;
}
stream.WriteByte(flags);
stream.Write(dataBytes);
-
- // Add padding byte if needed.
- if ((size & 1) == 1)
- {
- stream.WriteByte(0);
- }
- }
-
- ///
- /// 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);
- }
+ RiffHelper.EndWriteChunk(stream, pos);
}
///
@@ -204,65 +191,17 @@ internal abstract class BitWriterBase
/// 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)
+ /// 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}");
- }
-
- // 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 (xmpProfile != null)
- {
- // Set xmp bit.
- flags |= 4;
- }
+ WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
- if (hasAlpha)
- {
- // Set alpha bit.
- flags |= 16;
- }
-
- if (iccProfileBytes != null)
- {
- // Set iccp flag.
- flags |= 32;
- }
-
- Span buf = this.scratchBuffer.Span.Slice(0, 4);
- stream.Write(WebpConstants.Vp8XMagicBytes);
- 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/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
index 5b4eab64a3..81530706d6 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;
@@ -72,7 +69,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)
{
@@ -116,7 +113,7 @@ internal class Vp8BitWriter : BitWriterBase
else
{
this.PutBit(v >= 9, 165);
- this.PutBit(!((v & 1) != 0), 145);
+ this.PutBit((v & 1) == 0, 145);
}
}
else
@@ -394,87 +391,28 @@ internal class Vp8BitWriter : BitWriterBase
}
}
- ///
- /// 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.
- /// The alpha channel data.
- /// Indicates, if the alpha data is compressed.
- public void WriteEncodedImageToStream(
- Stream stream,
- ExifProfile? exifProfile,
- XmpProfile? xmpProfile,
- IccProfile? iccProfile,
- uint width,
- uint height,
- bool hasAlpha,
- Span alphaData,
- bool alphaDataIsCompressed)
+ ///
+ public override void WriteEncodedImageToStream(Stream stream)
{
- 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!);
- }
-
- if (xmpProfile != null)
- {
- isVp8X = true;
- xmpBytes = xmpProfile.Data;
- riffSize += MetadataChunkSize(xmpBytes!);
- }
-
- if (iccProfile != null)
- {
- isVp8X = true;
- iccProfileBytes = iccProfile.ToByteArray();
- riffSize += MetadataChunkSize(iccProfileBytes);
- }
-
- if (hasAlpha)
- {
- isVp8X = true;
- riffSize += AlphaChunkSize(alphaData);
- }
-
- if (isVp8X)
- {
- riffSize += ExtendedFileChunkSize;
- }
+ 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);
- Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc);
+ Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(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;
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,59 +421,49 @@ internal class Vp8BitWriter : BitWriterBase
{
stream.WriteByte(0);
}
-
- if (exifProfile != null)
- {
- this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
- }
-
- if (xmpProfile != null)
- {
- this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
- }
}
- 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 +471,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 +527,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 Vp8EncIterator(this.enc);
int predsWidth = this.enc.PredsWidth;
do
@@ -627,18 +555,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 +577,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,56 +585,18 @@ internal class Vp8BitWriter : BitWriterBase
}
}
- bitWriter.PutUvMode(mb.UvMode);
+ this.PutUvMode(mb.UvMode);
}
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 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..dc867fa85e 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;
@@ -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.
@@ -98,9 +98,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];
@@ -122,76 +119,20 @@ 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, iccBytes, width, height, hasAlpha);
-
- if (iccBytes != null)
- {
- this.WriteColorProfile(stream, iccBytes);
- }
- }
// 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.
@@ -200,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);
- }
}
///
@@ -226,7 +157,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/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/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
index 61133142bf..2e7dd722fc 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
@@ -50,8 +50,10 @@ 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();
+
+ ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
{
int cacheBitsTmp = cacheBitsInitial;
@@ -76,21 +78,19 @@ internal static class BackwardReferenceEncoder
}
// Next, try with a color cache and update the references.
- cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp);
+ cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp);
if (cacheBitsTmp > 0)
{
BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst);
}
// Keep the best backward references.
- var histo = new Vp8LHistogram(worst, cacheBitsTmp);
+ using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(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 +102,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 OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits);
double bitCostTrace = histo.EstimateBits(stats, bitsEntropy);
if (bitCostTrace < bitCostBest)
{
@@ -123,7 +123,13 @@ 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,
+ Span colorCache,
+ ReadOnlySpan bgra,
+ uint quality,
+ Vp8LBackwardRefs refs,
+ int bestCacheBits)
{
int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits;
if (cacheBitsMax == 0)
@@ -134,11 +140,11 @@ internal static class BackwardReferenceEncoder
double entropyMin = MaxEntropy;
int pos = 0;
- var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
- var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1];
- 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] = new Vp8LHistogram(paletteCodeBits: i);
+ histos[i].PaletteCodeBits = i;
colorCache[i] = new ColorCache(i);
}
@@ -149,10 +155,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 +224,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 +272,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 +286,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();
@@ -441,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);
@@ -693,10 +699,8 @@ internal static class BackwardReferenceEncoder
bestLength = MaxLength;
break;
}
- else
- {
- bestLength = currLength;
- }
+
+ bestLength = currLength;
}
}
}
@@ -775,7 +779,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/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
index c99e8fe6e2..beebc48abc 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 OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(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..3a96362cfd 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
@@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License.
#nullable disable
+using System.Buffers;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@@ -27,19 +29,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;
- 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));
- }
+ const int entropyCombineNumBins = BinSize;
+
+ 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);
// Construct the histograms from the backward references.
HistogramBuild(xSize, histoBits, refs, origHisto);
@@ -50,18 +61,17 @@ 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);
}
- 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, Span 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,17 +157,20 @@ 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;
@@ -166,7 +178,7 @@ internal static class HistogramEncoder
}
else
{
- histograms[i] = (Vp8LHistogram)origHistogram.DeepClone();
+ origHistogram.CopyTo(histograms[i]);
histogramSymbols[i] = (ushort)clusterId++;
}
}
@@ -184,11 +196,11 @@ internal static class HistogramEncoder
}
private static void HistogramCombineEntropyBin(
- List histograms,
+ Vp8LHistogramSet histograms,
Span clusters,
- ushort[] clusterMappings,
+ Span clusterMappings,
Vp8LHistogram curCombo,
- ushort[] binMap,
+ ReadOnlySpan binMap,
int numBins,
double combineCostFactor)
{
@@ -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,13 +248,11 @@ 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;
indicesToRemove.Add(idx);
@@ -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]);
}
}
@@ -266,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;
@@ -293,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++)
@@ -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);
@@ -398,12 +408,12 @@ 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[bestIdx1].BitCost = histoPriorityList[0].CostCombo;
histograms[bestIdx2] = null;
numUsed--;
@@ -418,7 +428,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 +449,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 +472,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++)
{
@@ -509,11 +516,11 @@ internal static class HistogramEncoder
// Remove pairs intersecting the just combined best pair.
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 +543,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 +587,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 +610,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 +653,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..d6b10ada55 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,21 +23,22 @@ 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,
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 469e4c9ab0..4fdbb31d37 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;
@@ -235,26 +237,60 @@ 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)
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)
+ {
+ WebpMetadata webpMetadata = metadata.GetWebpMetadata();
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ }
+ }
+
+ 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, width, height);
+ bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height);
// Write the image size.
this.WriteImageSize(width, height);
@@ -263,35 +299,60 @@ internal class Vp8LEncoder : IDisposable
this.WriteAlphaAndVersion(hasAlpha);
// Encode the main image stream.
- this.EncodeStream(image);
+ this.EncodeStream(frame);
+
+ this.bitWriter.Finish();
+
+ long prevPosition = 0;
+
+ if (hasAnimation)
+ {
+ WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
+
+ // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
+ 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.
- this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
+ this.bitWriter.WriteEncodedImageToStream(stream);
+
+ if (hasAnimation)
+ {
+ RiffHelper.EndWriteChunk(stream, prevPosition);
+ }
}
///
/// 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();
+ int size = this.bitWriter.NumBytes;
if (size >= pixelCount)
{
// Compressing would not yield in smaller data -> leave the data uncompressed.
@@ -333,12 +394,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();
@@ -425,9 +486,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);
}
@@ -447,14 +508,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);
@@ -589,15 +650,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 OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(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;
@@ -676,11 +743,9 @@ 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;
- bitWriterBest = tmp;
+ (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
}
isFirstIteration = false;
@@ -787,13 +852,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 +893,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 +946,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
{
+ // TODO: Allocations. This method is called in a loop.
int i;
byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes];
short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes];
@@ -996,7 +1057,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 +1074,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 +1090,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);
}
}
@@ -1149,35 +1215,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.
@@ -1379,10 +1451,8 @@ internal class Vp8LEncoder : IDisposable
useLut = false;
break;
}
- else
- {
- buffer[ind] = (uint)j;
- }
+
+ buffer[ind] = (uint)j;
}
if (useLut)
@@ -1591,14 +1661,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,13 +1677,25 @@ 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;
}
}
+ // TODO: Allocations.
int end = 5 * histogramImage.Count;
for (int i = 0; i < end; i++)
{
@@ -1629,8 +1709,9 @@ internal class Vp8LEncoder : IDisposable
}
// Create Huffman trees.
+ // 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++)
{
@@ -1682,8 +1763,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 +1811,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 +1819,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)]
@@ -1812,9 +1911,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();
}
}
@@ -1823,9 +1922,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/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
index 5ec3f0d53d..f473977908 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
@@ -1,63 +1,56 @@
// 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 abstract unsafe class Vp8LHistogram
{
private const uint NonTrivialSym = 0xffffffff;
+ 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;
+ 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;
///
/// Initializes a new instance of the class.
///
- /// The histogram to create an instance from.
- private Vp8LHistogram(Vp8LHistogram other)
- : this(other.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;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
+ /// The base pointer to the backing memory.
/// The backward references to initialize the histogram with.
/// The palette code bits.
- public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits)
- : this(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.
///
+ /// The base pointer to the backing memory.
/// The palette code bits.
- public Vp8LHistogram(int paletteCodeBits)
+ protected Vp8LHistogram(uint* basePointer, int paletteCodeBits)
{
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.red = basePointer;
+ 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;
}
///
@@ -85,22 +78,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable
///
public double BlueCost { get; set; }
- public uint[] Red { get; }
+ public Span Red => new(this.red, RedSize);
- public uint[] Blue { get; }
+ public Span Blue => new(this.blue, BlueSize);
- public uint[] Alpha { get; }
+ public Span Alpha => new(this.alpha, AlphaSize);
- public uint[] Literal { get; }
+ public Span Distance => new(this.distance, DistanceSize);
- public uint[] Distance { get; }
+ public Span Literal => new(this.literal, LiteralSize);
public uint TrivialSymbol { get; set; }
- public bool[] IsUsed { get; }
+ private Span IsUsedSpan => new(this.isUsed, UsedSize);
+
+ private Span TotalSpan => new(this.red, BufferSize);
+
+ [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 IDeepCloneable DeepClone() => new Vp8LHistogram(this);
+ public void Clear()
+ {
+ this.TotalSpan.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 +138,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 +192,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 +206,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 +263,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 +276,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 +299,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