Browse Source

Merge branch 'main' into MakerProfileResolver

pull/2566/head
JoseEliasSantos 3 years ago
committed by GitHub
parent
commit
412e451d64
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 124
      src/ImageSharp/Common/Helpers/RiffHelper.cs
  2. 8
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  3. 47
      src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
  4. 160
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  5. 2
      src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
  6. 14
      src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
  7. 2
      src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
  8. 20
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  9. 22
      src/ImageSharp/Formats/Png/PngBlendMethod.cs
  10. 7
      src/ImageSharp/Formats/Png/PngChunk.cs
  11. 65
      src/ImageSharp/Formats/Png/PngChunkType.cs
  12. 8
      src/ImageSharp/Formats/Png/PngConstants.cs
  13. 468
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  14. 25
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  15. 446
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  16. 5
      src/ImageSharp/Formats/Png/PngFormat.cs
  17. 62
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  18. 9
      src/ImageSharp/Formats/Png/PngMetadata.cs
  19. 359
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  20. 26
      src/ImageSharp/Formats/Png/PngThrowHelper.cs
  21. 2
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  22. 40
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  23. 48
      src/ImageSharp/Formats/Webp/AnimationFrameData.cs
  24. 239
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  25. 240
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  26. 91
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  27. 37
      src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs
  28. 140
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  29. 113
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  30. 60
      src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
  31. 2
      src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
  32. 16
      src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
  33. 193
      src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
  34. 18
      src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
  35. 9
      src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
  36. 6
      src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
  37. 299
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  38. 308
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
  39. 110
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs
  40. 113
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  41. 11
      src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
  42. 153
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  43. 187
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  44. 6
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  45. 158
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  46. 2
      src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
  47. 63
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  48. 11
      src/ImageSharp/Formats/Webp/WebpChunkType.cs
  49. 43
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  50. 9
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  51. 29
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  52. 2
      src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
  53. 2
      src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
  54. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  55. 62
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  56. 6
      src/ImageSharp/Formats/Webp/WebpFormat.cs
  57. 19
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  58. 9
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  59. 1
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  60. 4
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  61. 17
      src/ImageSharp/Primitives/Rational.cs
  62. 13
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  63. 26
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  64. 34
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  65. 35
      tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
  66. 14
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  67. 2
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
  68. 29
      tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
  69. 14
      tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs
  70. 14
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  71. 43
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  72. 4
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  73. 12
      tests/ImageSharp.Tests/TestImages.cs
  74. 28
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  75. 19
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  76. 8
      tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs
  77. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png
  78. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png
  79. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png
  80. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png
  81. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png
  82. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png
  83. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png
  84. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png
  85. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png
  86. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png
  87. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png
  88. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png
  89. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png
  90. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png
  91. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png
  92. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png
  93. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png
  94. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png
  95. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png
  96. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png
  97. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png
  98. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png
  99. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png
  100. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png

124
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
{
/// <summary>
/// The header bytes identifying RIFF file.
/// </summary>
private const uint RiffFourCc = 0x52_49_46_46;
public static void WriteRiffFile(Stream stream, string formType, Action<Stream> func) =>
WriteChunk(stream, RiffFourCc, s =>
{
s.Write(Encoding.ASCII.GetBytes(formType));
func(s);
});
public static void WriteChunk(Stream stream, uint fourCc, Action<Stream> func)
{
Span<byte> 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<byte> data)
{
Span<byte> 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<TStruct>(Stream stream, uint fourCc, in TStruct chunk)
where TStruct : unmanaged
{
fixed (TStruct* ptr = &chunk)
{
WriteChunk(stream, fourCc, new Span<byte>(ptr, sizeof(TStruct)));
}
}
public static long BeginWriteChunk(Stream stream, uint fourCc)
{
Span<byte> 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<byte> 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);
}

8
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream
/// <inheritdoc/>
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;
}

47
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;
}
/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; }
/// <summary>
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; }
/// <summary>
/// Writes the acTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
}
/// <summary>
/// Parses the APngAnimationControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed acTL.</returns>
public static AnimationControl Parse(ReadOnlySpan<byte> data)
=> new(
numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
}

160
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;
}
/// <summary>
/// Gets the sequence number of the animation chunk, starting from 0
/// </summary>
public uint SequenceNumber { get; }
/// <summary>
/// Gets the width of the following frame
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the height of the following frame
/// </summary>
public uint Height { get; }
/// <summary>
/// Gets the X position at which to render the following frame
/// </summary>
public uint XOffset { get; }
/// <summary>
/// Gets the Y position at which to render the following frame
/// </summary>
public uint YOffset { get; }
/// <summary>
/// Gets the X limit at which to render the following frame
/// </summary>
public uint XMax => this.XOffset + this.Width;
/// <summary>
/// Gets the Y limit at which to render the following frame
/// </summary>
public uint YMax => this.YOffset + this.Height;
/// <summary>
/// Gets the frame delay fraction numerator
/// </summary>
public ushort DelayNumerator { get; }
/// <summary>
/// Gets the frame delay fraction denominator
/// </summary>
public ushort DelayDenominator { get; }
/// <summary>
/// Gets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposeOperation { get; }
/// <summary>
/// Gets the type of frame area rendering for this frame
/// </summary>
public PngBlendMethod BlendOperation { get; }
public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height);
/// <summary>
/// Validates the APng fcTL.
/// </summary>
/// <param name="header">The header.</param>
/// <exception cref="NotSupportedException">
/// Thrown if the image does pass validation.
/// </exception>
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)}");
}
}
/// <summary>
/// Writes the fcTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> 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;
}
/// <summary>
/// Parses the APngFrameControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed fcTL.</returns>
public static FrameControl Parse(ReadOnlySpan<byte> 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]);
}

2
src/ImageSharp/Formats/Png/PngHeader.cs → 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;
/// <summary>
/// Represents the png header chunk.

14
src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs → 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;
/// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
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
/// </summary>
/// <param name="data">The data buffer.</param>
/// <returns>The parsed PhysicalChunkData.</returns>
public static PhysicalChunkData Parse(ReadOnlySpan<byte> data)
public static PngPhysical Parse(ReadOnlySpan<byte> 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);
}
/// <summary>
@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData
/// </summary>
/// <param name="meta">The metadata.</param>
/// <returns>The constructed PngPhysicalChunkData instance.</returns>
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);
}
/// <summary>

2
src/ImageSharp/Formats/Png/PngTextData.cs → 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;
/// <summary>
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks.

20
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
/// <summary>
/// Gets the png format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngMetadata"/>.</returns>
public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance);
public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
}

22
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;
/// <summary>
/// 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.
/// </summary>
public enum PngBlendMethod
{
/// <summary>
/// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
/// </summary>
Source,
/// <summary>
/// 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].
/// </summary>
Over
}

7
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
/// </summary>
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;
}

65
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -9,15 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal enum PngChunkType : uint
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>IDAT (Multiple)</remarks>
Data = 0x49444154U,
/// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
/// </summary>
/// <remarks>IEND (Single)</remarks>
End = 0x49454E44U,
/// <summary>
@ -25,34 +27,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
/// </summary>
/// <remarks>IHDR (Single)</remarks>
Header = 0x49484452U,
/// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
/// <remarks>PLTE (Single)</remarks>
Palette = 0x504C5445U,
/// <summary>
/// The eXIf data chunk which contains the Exif profile.
/// </summary>
/// <remarks>eXIF (Single)</remarks>
Exif = 0x65584966U,
/// <summary>
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
/// </summary>
/// <remarks>gAMA (Single)</remarks>
Gamma = 0x67414D41U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>pHYs (Single)</remarks>
Physical = 0x70485973U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>tEXT (Multiple)</remarks>
Text = 0x74455874U,
/// <summary>
@ -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.
/// </summary>
/// <remarks>zTXt (Multiple)</remarks>
CompressedText = 0x7A545874U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>iTXt (Multiple)</remarks>
InternationalText = 0x69545874U,
/// <summary>
/// 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).
/// </summary>
/// <remarks>tRNS (Single)</remarks>
Transparency = 0x74524E53U,
/// <summary>
/// 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).
/// </summary>
/// <remarks>tIME (Single)</remarks>
Time = 0x74494d45,
/// <summary>
/// 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.
/// </summary>
/// <remarks>bKGD (Single)</remarks>
Background = 0x624b4744,
/// <summary>
/// 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.
/// </summary>
/// <remarks>iCCP (Single)</remarks>
EmbeddedColorProfile = 0x69434350,
/// <summary>
/// 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.
/// </summary>
/// <remarks>sBIT (Single)</remarks>
SignificantBits = 0x73424954,
/// <summary>
/// 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.
/// </summary>
/// <remarks>sRGB (Single)</remarks>
StandardRgbColourSpace = 0x73524742,
/// <summary>
/// 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.
/// </summary>
/// <remarks>hIST (Single)</remarks>
Histogram = 0x68495354,
/// <summary>
/// The sPLT chunk contains the suggested palette.
/// This chunk contains the suggested palette.
/// </summary>
/// <remarks>sPLT (Single)</remarks>
SuggestedPalette = 0x73504c54,
/// <summary>
/// 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.
/// </summary>
/// <remarks>cHRM (Single)</remarks>
Chroma = 0x6348524d,
/// <summary>
/// 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.
/// </summary>
/// <remarks>acTL (Single, APNG)</remarks>
AnimationControl = 0x6163544cU,
/// <summary>
/// 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.
/// </summary>
/// <remarks>fcTL (Multiple, APNG)</remarks>
FrameControl = 0x6663544cU,
/// <summary>
/// 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.
/// </summary>
/// <remarks>fdAT (Multiple, APNG)</remarks>
FrameData = 0x66644154U,
/// <summary>
/// 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
/// </summary>
/// <remarks>CgBI</remarks>
ProprietaryApple = 0x43674249
}

8
src/ImageSharp/Formats/Png/PngConstants.cs

@ -28,12 +28,12 @@ internal static class PngConstants
/// <summary>
/// The list of mimetypes that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png" };
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png", "image/apng" };
/// <summary>
/// The list of file extensions that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png" };
public static readonly IEnumerable<string> FileExtensions = new[] { "png", "apng" };
/// <summary>
/// The header bytes as a big-endian coded ulong.
@ -43,7 +43,7 @@ internal static class PngConstants
/// <summary>
/// The dictionary of available color types.
/// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>
public static readonly Dictionary<PngColorType, byte[]> 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
/// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new byte[]
public static ReadOnlySpan<byte> XmpKeyword => new[]
{
(byte)'X',
(byte)'M',

468
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;
/// <summary>
/// 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.
/// </summary>
private readonly uint maxFrames;
/// <summary>
/// Whether the metadata should be ignored when the image is being decoded.
/// </summary>
private readonly bool skipMetadata;
/// <summary>
/// Gets or sets a value indicating whether to read the IHDR and tRNS chunks only.
/// Whether to read the IHDR and tRNS chunks only.
/// </summary>
private readonly bool colorMetadataOnly;
@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream currentStream;
private BufferedReadStream currentStream = null!;
/// <summary>
/// The png header.
/// </summary>
private PngHeader header;
/// <summary>
/// The png animation control.
/// </summary>
private AnimationControl animationControl;
/// <summary>
/// The number of bytes per pixel.
/// </summary>
@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary>
/// The palette containing color information for indexed png's.
/// </summary>
private byte[] palette;
private byte[] palette = null!;
/// <summary>
/// The palette containing alpha channel color information for indexed png's.
/// </summary>
private byte[] paletteAlpha;
private byte[] paletteAlpha = null!;
/// <summary>
/// Previous scanline processed.
/// </summary>
private IMemoryOwner<byte> previousScanline;
private IMemoryOwner<byte> previousScanline = null!;
/// <summary>
/// The current scanline that is being processed.
/// </summary>
private IMemoryOwner<byte> scanline;
/// <summary>
/// The index of the current scanline being processed.
/// </summary>
private int currentRow = Adam7.FirstRow[0];
/// <summary>
/// The current number of bytes read in the current scanline.
/// </summary>
private int currentRowBytesRead;
private IMemoryOwner<byte> scanline = null!;
/// <summary>
/// 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<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint frameCount = 0;
ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
this.currentStream.Skip(8);
Image<TPixel> image = null;
Image<TPixel>? image = null;
FrameControl? previousFrameControl = null;
FrameControl? currentFrameControl = null;
ImageFrame<TPixel>? previousFrame = null;
ImageFrame<TPixel>? currentFrame = null;
Span<byte> 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
/// <inheritdoc/>
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
FrameControl? lastFrameControl = null;
Span<byte> 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
/// <param name="bits">The number of bits per value.</param>
/// <param name="buffer">The new array.</param>
/// <returns>The resulting <see cref="ReadOnlySpan{Byte}"/> array.</returns>
private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, out IMemoryOwner<byte> buffer)
private bool TryScaleUpTo8BitArray(ReadOnlySpan<byte> source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner<byte>? buffer)
{
if (bits >= 8)
{
@ -438,7 +537,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <param name="data">The data containing physical data.</param>
private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan<byte> 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
/// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <param name="metadata">The metadata information for the image</param>
/// <param name="frameControl">The frame control information for the frame</param>
/// <param name="image">The image that we will populate</param>
private void InitializeImage<TPixel>(ImageMetadata metadata, out Image<TPixel> image)
private void InitializeImage<TPixel>(ImageMetadata metadata, FrameControl frameControl, out Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
image = Image.CreateUninitialized<TPixel>(
@ -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<byte>(this.bytesPerScanline, AllocationOptions.Clean);
}
/// <summary>
/// Initializes the image and various buffers needed for processing
/// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <param name="previousFrameControl">The frame control information for the previous frame.</param>
/// <param name="currentFrameControl">The frame control information for the current frame.</param>
/// <param name="image">The image that we will populate</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="frame">The created frame</param>
private void InitializeFrame<TPixel>(
FrameControl previousFrameControl,
FrameControl currentFrameControl,
Image<TPixel> image,
ImageFrame<TPixel>? previousFrame,
out ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel> 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<byte>(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
}
/// <summary>
/// Calculates the correct number of bits per pixel for the given color type.
/// </summary>
@ -558,24 +702,32 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Reads the scanlines within the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="chunk">The png chunk containing the compressed scanline data.</param>
/// <param name="chunkLength">The length of the chunk that containing the compressed scanline data.</param>
/// <param name="image"> The pixel data.</param>
/// <param name="pngMetadata">The png metadata</param>
/// <param name="getData">A delegate to get more data from the inner stream for <see cref="ZlibInflateStream"/>.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void ReadScanlines<TPixel>(PngChunk chunk, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
private void ReadScanlines<TPixel>(
int chunkLength,
ImageFrame<TPixel> image,
PngMetadata pngMetadata,
Func<int> getData,
in FrameControl frameControl,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The image to decode to.</param>
/// <param name="imageFrame">The image frame to decode to.</param>
/// <param name="pngMetadata">The png metadata</param>
/// <param name="cancellationToken">The CancellationToken</param>
private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
private void DecodePixelData<TPixel>(
FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
while (this.currentRow < this.header.Height)
int currentRow = (int)frameControl.YOffset;
int currentRowBytesRead = 0;
int height = (int)frameControl.YMax;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty;
if (frameControl.BlendOperation == PngBlendMethod.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
}
while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
Span<byte> scanlineSpan = this.scanline.GetSpan();
while (this.currentRowBytesRead < this.bytesPerScanline)
int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1;
Span<byte> 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();
}
/// <summary>
/// Decodes the raw interlaced pixel data row by row
/// <see href="https://github.com/juehv/DentalImageViewer/blob/8a1a4424b15d6cc453b5de3f273daf3ff5e3a90d/DentalImageViewer/lib/jiu-0.14.3/net/sourceforge/jiu/codecs/PNGCodec.java"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The current image.</param>
/// <param name="imageFrame">The current image frame.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
private void DecodeInterlacedPixelData<TPixel>(
in FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset;
int currentRowBytesRead = 0;
int pass = 0;
int width = this.header.Width;
Buffer2D<TPixel> imageBuffer = image.PixelBuffer;
int width = (int)frameControl.Width;
int endRow = (int)frameControl.YMax;
Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = Span<TPixel>.Empty;
if (frameControl.BlendOperation == PngBlendMethod.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(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<byte> scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline);
Span<byte> prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline);
@ -714,12 +904,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
Span<TPixel> 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();
}
/// <summary>
/// Processes the de-filtered scanline filling the image pixel data
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="frameControl">The frame control</param>
/// <param name="currentRow">The index of the current scanline being processed.</param>
/// <param name="scanline">The de-filtered scanline</param>
/// <param name="pixels">The image</param>
/// <param name="pngMetadata">The png metadata.</param>
private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetadata pngMetadata)
/// <param name="blendRowBuffer">A span used to temporarily hold the decoded row pixel data for alpha blending.</param>
private void ProcessDefilteredScanline<TPixel>(
in FrameControl frameControl,
int currentRow,
ReadOnlySpan<byte> scanline,
ImageFrame<TPixel> pixels,
PngMetadata pngMetadata,
Span<TPixel> blendRowBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow);
Span<TPixel> destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
Span<TPixel> rowSpan = blend
? blendRowBuffer
: destination;
// Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
ReadOnlySpan<byte> trimmed = scanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null;
IMemoryOwner<byte>? buffer = null;
try
{
// TODO: The allocation here could be per frame, not per scanline.
ReadOnlySpan<byte> 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<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
blender.Blend<TPixel>(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
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="rowSpan">The current image row.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="scanline">The de-filtered scanline</param>
/// <param name="destination">The current image row.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="blendRowBuffer">A span used to temporarily hold the decoded row pixel data for alpha blending.</param>
/// <param name="pixelOffset">The column start index. Always 0 for none interlaced images.</param>
/// <param name="increment">The column increment. Always 1 for none interlaced images.</param>
private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1)
private void ProcessInterlacedDefilteredScanline<TPixel>(
in FrameControl frameControl,
ReadOnlySpan<byte> scanline,
Span<TPixel> destination,
PngMetadata pngMetadata,
Span<TPixel> blendRowBuffer,
int pixelOffset = 0,
int increment = 1)
where TPixel : unmanaged, IPixel<TPixel>
{
bool blend = frameControl.BlendOperation == PngBlendMethod.Over;
Span<TPixel> rowSpan = blend
? blendRowBuffer
: destination;
// Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline[1..];
ReadOnlySpan<byte> trimmed = scanline[1..];
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
IMemoryOwner<byte> buffer = null;
IMemoryOwner<byte>? buffer = null;
try
{
ReadOnlySpan<byte> 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<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
blender.Blend<TPixel>(this.configuration, destination, destination, rowSpan, 1f);
}
}
finally
{
@ -996,6 +1249,31 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
}
}
/// <summary>
/// Reads a animation control chunk from the data.
/// </summary>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> data)
{
this.animationControl = AnimationControl.Parse(data);
pngMetadata.RepeatCount = this.animationControl.NumberPlays;
}
/// <summary>
/// Reads a header chunk from the data.
/// </summary>
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private FrameControl ReadFrameControlChunk(ReadOnlySpan<byte> data)
{
FrameControl fcTL = FrameControl.Parse(data);
fcTL.Validate(this.header);
return fcTL;
}
/// <summary>
/// Reads a header chunk from the data.
/// </summary>
@ -1083,7 +1361,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ReadOnlySpan<byte> 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<byte> 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
/// <param name="encoding">The string encoding to use.</param>
/// <param name="value">The uncompressed value.</param>
/// <returns>The <see cref="bool"/>.</returns>
private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, out string value)
private bool TryUncompressTextData(ReadOnlySpan<byte> 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<byte> 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;
}
/// <summary>
/// Reads the next data chunk and skip sequence number.
/// </summary>
/// <returns>Count of bytes in the next data chunk, or 0 if there are no more data chunks left.</returns>
private int ReadNextDataChunkAndSkipSeq()
{
int length = this.ReadNextDataChunk();
if (this.ReadNextDataChunk() is 0)
{
return length;
}
this.currentStream.Position += 4; // Skip sequence number
return length - 4;
}
/// <summary>
/// Reads a chunk from the stream.
/// </summary>
@ -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;
}

25
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;
/// <summary>
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
/// </summary>
public enum PngDisposalMethod
{
/// <summary>
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
/// </summary>
None,
/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
/// </summary>
Background,
/// <summary>
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
/// </summary>
Previous
}

446
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
/// <summary>
/// The raw data of previous scanline.
/// </summary>
private IMemoryOwner<byte> previousScanline;
private IMemoryOwner<byte> previousScanline = null!;
/// <summary>
/// The raw data of current scanline.
/// </summary>
private IMemoryOwner<byte> currentScanline;
private IMemoryOwner<byte> currentScanline = null!;
/// <summary>
/// The color profile name.
/// </summary>
private const string ColorProfileName = "ICC Profile";
/// <summary>
/// The encoder quantizer, if present.
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary>
@ -122,6 +126,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
}
/// <summary>
@ -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<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
stream.Write(PngConstants.HeaderBytes);
ImageFrame<TPixel>? clonedFrame = null;
ImageFrame<TPixel> 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<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
stream.Write(PngConstants.HeaderBytes);
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? 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<TPixel>? 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();
}
/// <inheritdoc />
@ -179,18 +232,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
this.previousScanline = null;
this.currentScanline = null;
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> =>
image.ProcessPixelRows(accessor =>
/// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone)
where TPixel : unmanaged, IPixel<TPixel>
=> 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImageAndUpdateBitDepth<TPixel>(
Image<TPixel> image,
Image<TPixel> clonedImage)
private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
PngMetadata metadata,
ImageFrame<TPixel> frame,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> 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<TPixel>? 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<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : unmanaged, IPixel<TPixel>
{
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> 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
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel>? quantized, int row)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> rowSpan,
ref Span<byte> filter,
ref Span<byte> attempt,
IndexedImageFrame<TPixel> quantized,
IndexedImageFrame<TPixel>? quantized,
int row)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -574,6 +615,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
}
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="framesCount">The number of frames.</param>
/// <param name="playsCount">The number of times to loop this APNG.</param>
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);
}
/// <summary>
/// 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
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel> quantized)
private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>? quantized)
where TPixel : unmanaged, IPixel<TPixel>
{
if (quantized is null)
@ -640,14 +696,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param>
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);
}
/// <summary>
@ -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
}
}
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageFrame">The image frame.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
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;
}
/// <summary>
/// Writes the pixel information to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="pixels">The frame.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
/// <param name="isFrame">Is writing fdAT or IDAT.</param>
private uint WriteDataChunks<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
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;
}
/// <summary>
@ -1009,13 +1105,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Encodes the pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodePixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<byte> filter = filterBuffer.GetSpan();
Span<byte> 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="frame">The image frame.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> image, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
Buffer2D<TPixel> pixelBuffer = image.Frames.RootFrame.PixelBuffer;
int width = (int)frameControl.XMax;
int height = (int)frameControl.YMax;
Buffer2D<TPixel> 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<TPixel> 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodeAdam7IndexedPixels<TPixel>(FrameControl frameControl, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
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<byte> filter = filterBuffer.GetSpan();
Span<byte> 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<byte> 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
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
/// <param name="data">The <see cref="Span{Byte}"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param>
private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int offset, int length)
@ -1189,6 +1290,38 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
stream.Write(buffer, 0, 4); // write the crc
}
/// <summary>
/// Writes a frame data chunk of a specified length to the stream at the given offset.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
/// <param name="data">The <see cref="Span{Byte}"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param>
private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span<byte> data, int offset, int length)
{
Span<byte> 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
}
/// <summary>
/// Calculates the scanline length.
/// </summary>
@ -1198,7 +1331,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </returns>
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
/// <param name="encoder">The png encoder.</param>
/// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param>
/// <param name="image">The image.</param>
private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
Image<TPixel> image)
PngMetadata metadata,
ImageFrame<TPixel> frame,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> 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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.Configuration);
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(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());
}
/// <summary>
@ -1323,25 +1459,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private static byte CalculateBitDepth<TPixel>(
PngColorType colorType,
byte bitDepth,
IndexedImageFrame<TPixel> quantizedFrame)
IndexedImageFrame<TPixel>? quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
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
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> 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
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> 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<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);

5
src/ImageSharp/Formats/Png/PngFormat.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the png format.
/// </summary>
public sealed class PngFormat : IImageFormat<PngMetadata>
public sealed class PngFormat : IImageFormat<PngMetadata, PngFrameMetadata>
{
private PngFormat()
{
@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat<PngMetadata>
/// <inheritdoc/>
public PngMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}

62
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;
/// <summary>
/// Provides APng specific metadata information for the image frame.
/// </summary>
public class PngFrameMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
public PngFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private PngFrameMetadata(PngFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
}
/// <summary>
/// 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.
/// </summary>
public Rational FrameDelay { get; set; }
/// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposalMethod { get; set; }
/// <summary>
/// Gets or sets the type of frame area rendering for this frame
/// </summary>
public PngBlendMethod BlendMethod { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
/// <param name="frameControl">The chunk to create an instance from.</param>
internal void FromChunk(in FrameControl frameControl)
{
this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
this.DisposalMethod = frameControl.DisposeOperation;
this.BlendMethod = frameControl.BlendOperation;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
}

9
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;
/// <summary>
@ -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
/// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
/// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int RepeatCount { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);
}

359
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<TPixel>(
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<ushort, L16>(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<byte, L8>(ref luminance));
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
L16 transparent = transparentColor.Value.ToPixel<L16>();
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<L8>().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<TPixel> =>
ProcessInterlacedGrayscaleScanline(
bitDepth,
frameControl,
scanlineSpan,
rowSpan,
0,
1,
transparentColor);
public static void ProcessInterlacedGrayscaleScanline<TPixel>(
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint pixelOffset,
@ -91,17 +41,18 @@ internal static class PngScanlineProcessor
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<ushort, L16>(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<byte, L8>(ref luminance));
@ -121,12 +72,12 @@ internal static class PngScanlineProcessor
return;
}
if (header.BitDepth == 16)
if (bitDepth == 16)
{
L16 transparent = transparentColor.Value.ToPixel<L16>();
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<L8>().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<TPixel>(
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint bytesPerPixel,
uint bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> =>
ProcessInterlacedGrayscaleWithAlphaScanline(
bitDepth,
frameControl,
scanlineSpan,
rowSpan,
0,
1,
bytesPerPixel,
bytesPerSample);
public static void ProcessInterlacedGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint pixelOffset,
@ -202,15 +132,16 @@ internal static class PngScanlineProcessor
uint bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(
in PngHeader header,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
ReadOnlyMemory<Color>? palette)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> =>
ProcessInterlacedPaletteScanline(
frameControl,
scanlineSpan,
rowSpan,
0,
1,
palette);
public static void ProcessInterlacedPaletteScanline<TPixel>(
in PngHeader header,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> 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<TPixel>(
Configuration configuration,
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample,
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width);
}
return;
}
if (header.BitDepth == 16)
{
Rgb48 transparent = transparentColor.Value.ToPixel<Rgb48>();
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<Rgb24>();
Rgba32 rgba32 = default;
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(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<TPixel> =>
ProcessInterlacedRgbScanline(
configuration,
bitDepth,
frameControl,
scanlineSpan,
rowSpan,
0,
1,
bytesPerPixel,
bytesPerSample,
transparentColor);
public static void ProcessInterlacedRgbScanline<TPixel>(
in PngHeader header,
Configuration configuration,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint pixelOffset,
@ -374,18 +242,19 @@ internal static class PngScanlineProcessor
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.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 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<TPixel>(
Configuration configuration,
in PngHeader header,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width);
}
}
where TPixel : unmanaged, IPixel<TPixel> =>
ProcessInterlacedRgbaScanline(
configuration,
bitDepth,
frameControl,
scanlineSpan,
rowSpan,
0,
1,
bytesPerPixel,
bytesPerSample);
public static void ProcessInterlacedRgbaScanline<TPixel>(
in PngHeader header,
Configuration configuration,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint pixelOffset,
@ -495,15 +361,15 @@ internal static class PngScanlineProcessor
int bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.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));

26
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.");

2
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);

40
src/ImageSharp/Formats/Webp/AlphaEncoder.cs

@ -19,7 +19,7 @@ internal static class AlphaEncoder
/// Data is either compressed as lossless webp image or uncompressed.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <param name="skipMetadata">Whether to skip metadata encoding.</param>
@ -27,7 +27,7 @@ internal static class AlphaEncoder
/// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns>
public static IMemoryOwner<byte> EncodeAlpha<TPixel>(
Image<TPixel> image,
ImageFrame<TPixel> frame,
Configuration configuration,
MemoryAllocator memoryAllocator,
bool skipMetadata,
@ -35,9 +35,9 @@ internal static class AlphaEncoder
out int size)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
IMemoryOwner<byte> alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator);
int width = frame.Width;
int height = frame.Height;
IMemoryOwner<byte> 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<Rgba32> alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan());
using ImageFrame<Rgba32> 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param>
/// <returns>The transparency image.</returns>
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData)
/// <returns>The transparency frame.</returns>
private static ImageFrame<Rgba32> DispatchAlphaToGreen<TPixel>(ImageFrame<TPixel> frame, Span<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
Image<Rgba32> alphaAsImage = new(width, height);
int width = frame.Width;
int height = frame.Height;
ImageFrame<Rgba32> alphaAsFrame = new ImageFrame<Rgba32>(Configuration.Default, width, height);
for (int y = 0; y < height; y++)
{
Memory<Rgba32> rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y);
Memory<Rgba32> rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y);
Span<Rgba32> pixelRow = rowBuffer.Span;
Span<byte> 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;
}
/// <summary>
/// Extract the alpha data of the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns>
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator)
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(ImageFrame<TPixel> frame, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
int height = image.Height;
int width = image.Width;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int height = frame.Height;
int width = frame.Width;
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
Span<byte> alphaData = alphaDataBuffer.GetSpan();

48
src/ImageSharp/Formats/Webp/AnimationFrameData.cs

@ -1,48 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
internal struct AnimationFrameData
{
/// <summary>
/// The animation chunk size.
/// </summary>
public uint DataSize;
/// <summary>
/// The X coordinate of the upper left corner of the frame is Frame X * 2.
/// </summary>
public uint X;
/// <summary>
/// The Y coordinate of the upper left corner of the frame is Frame Y * 2.
/// </summary>
public uint Y;
/// <summary>
/// The width of the frame.
/// </summary>
public uint Width;
/// <summary>
/// The height of the frame.
/// </summary>
public uint Height;
/// <summary>
/// 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.
/// </summary>
public uint Duration;
/// <summary>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public AnimationBlendingMethod BlendingMethod;
/// <summary>
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public AnimationDisposalMethod DisposalMethod;
}

239
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;
/// <summary>
/// Buffer to write to.
/// </summary>
private byte[] buffer;
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly
/// <summary>
/// Initializes a new instance of the <see cref="BitWriterBase"/> class.
/// </summary>
@ -41,17 +36,23 @@ internal abstract class BitWriterBase
public byte[] Buffer => this.buffer;
/// <summary>
/// Gets the number of bytes of the encoded image data.
/// </summary>
/// <returns>The number of bytes of the image data.</returns>
public abstract int NumBytes { get; }
/// <summary>
/// Writes the encoded bytes of the image to the stream. Call Finish() before this.
/// </summary>
/// <param name="stream">The stream to write to.</param>
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));
/// <summary>
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
/// </summary>
/// <param name="dest">The destination buffer.</param>
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest);
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest);
/// <summary>
/// Resizes the buffer to write to.
@ -59,12 +60,6 @@ internal abstract class BitWriterBase
/// <param name="extraSize">The extra size in bytes needed.</param>
public abstract void BitWriterResize(int extraSize);
/// <summary>
/// Returns the number of bytes of the encoded image data.
/// </summary>
/// <returns>The number of bytes of the image data.</returns>
public abstract int NumBytes();
/// <summary>
/// Flush leftover bits.
/// </summary>
@ -84,63 +79,89 @@ internal abstract class BitWriterBase
}
/// <summary>
/// Writes the RIFF header to the stream.
/// Write the trunks before data trunk.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="riffSize">The block length.</param>
protected void WriteRiffHeader(Stream stream, uint riffSize)
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
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());
}
}
}
/// <summary>
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
protected static uint MetadataChunkSize(byte[] metadataBytes)
{
uint metaSize = (uint)metadataBytes.Length;
return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
}
/// <param name="stream">The stream to write to.</param>
public abstract void WriteEncodedImageToStream(Stream stream);
/// <summary>
/// Calculates the chunk size of a alpha chunk.
/// Write the trunks after data trunk.
/// </summary>
/// <param name="alphaBytes">The alpha chunk bytes.</param>
/// <returns>The alpha data chunk size in bytes.</returns>
protected static uint AlphaChunkSize(Span<byte> alphaBytes)
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
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);
}
/// <summary>
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// Writes the animation parameter(<see cref="WebpChunkType.AnimationParameter"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="metadataBytes">The metadata profile's bytes.</param>
/// <param name="chunkType">The chuck type to write.</param>
protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
/// <param name="background">
/// 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.
/// </param>
/// <param name="loopCount">The number of times to loop the animation. If it is 0, this means infinitely.</param>
public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount)
{
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)metadataBytes.Length;
Span<byte> 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);
}
/// <summary>
@ -149,53 +170,19 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param>
/// <param name="dataBytes">The alpha channel data bytes.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
public static void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{
uint size = (uint)dataBytes.Length + 1;
Span<byte> 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);
}
}
/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
{
uint size = (uint)iccProfileBytes.Length;
Span<byte> 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);
}
/// <summary>
@ -204,65 +191,17 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha)
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
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<byte> 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<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
chunk.WriteTo(stream);
}
}

240
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
}
/// <inheritdoc/>
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
}
}
/// <summary>
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="alphaData">The alpha channel data.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
public void WriteEncodedImageToStream(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile,
IccProfile? iccProfile,
uint width,
uint height,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
/// <inheritdoc />
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<byte> 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<byte> 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<byte> vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize];
WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader);
BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size);
stream.Write(vp8ChunkHeader);
Span<byte> 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)

91
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;
}
/// <inheritdoc/>
public override int NumBytes => this.cur + ((this.used + 7) >> 3);
/// <summary>
/// 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);
}
/// <inheritdoc/>
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;
}
/// <summary>
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha)
/// <inheritdoc />
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<byte> scratchBuffer = stackalloc byte[WebpConstants.TagSize];
BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L);
stream.Write(scratchBuffer);
// Write Vp8 Header.
Span<byte> 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);
}
}
/// <summary>
@ -226,7 +157,7 @@ internal class Vp8LBitWriter : BitWriterBase
Span<byte> 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;

37
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;
}
/// <summary>
/// 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.
/// </summary>
public uint Background { get; }
/// <summary>
/// Gets number of times to loop the animation. If it is 0, this means infinitely.
/// </summary>
public ushort LoopCount { get; }
public void WriteTo(Stream stream)
{
Span<byte> buffer = stackalloc byte[6];
BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount);
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer);
}
}

140
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
{
/// <summary>
/// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags.
/// </summary>
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)
{
}
/// <summary>
/// Gets the animation chunk size.
/// </summary>
public uint DataSize { get; }
/// <summary>
/// Gets the X coordinate of the upper left corner of the frame is Frame X * 2.
/// </summary>
public uint X { get; }
/// <summary>
/// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2.
/// </summary>
public uint Y { get; }
/// <summary>
/// Gets the width of the frame.
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the height of the frame.
/// </summary>
public uint Height { get; }
/// <summary>
/// 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.
/// </summary>
public uint Duration { get; }
/// <summary>
/// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public WebpBlendingMethod BlendingMethod { get; }
/// <summary>
/// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public WebpDisposalMethod DisposalMethod { get; }
public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height);
/// <summary>
/// Writes the animation frame(<see cref="WebpChunkType.FrameData"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
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;
}
/// <summary>
/// Reads the animation frame header.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns>
public static WebpFrameData Parse(Stream stream)
{
Span<byte> 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;
}
}

113
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;
}
/// <summary>
/// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation.
/// </summary>
public bool HasAnimation { get; }
/// <summary>
/// Gets a value indicating whether the file contains XMP metadata.
/// </summary>
public bool HasXmp { get; }
/// <summary>
/// Gets a value indicating whether the file contains Exif metadata.
/// </summary>
public bool HasExif { get; }
/// <summary>
/// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha").
/// </summary>
public bool HasAlpha { get; }
/// <summary>
/// Gets a value indicating whether the file contains an 'ICCP' Chunk.
/// </summary>
public bool HasIcc { get; }
/// <summary>
/// Gets width of the canvas in pixels. (uint24)
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets height of the canvas in pixels. (uint24)
/// </summary>
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);
}
}

60
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.
/// </summary>
/// <returns>Best cache size.</returns>
private static int CalculateBestCacheSize(ReadOnlySpan<uint> bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits)
private static int CalculateBestCacheSize(
MemoryAllocator memoryAllocator,
Span<ColorCache> colorCache,
ReadOnlySpan<uint> 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<float> costManagerCosts = costManager.Costs.GetSpan();
Span<ushort> 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<uint> 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];

2
src/ImageSharp/Formats/Webp/Lossless/CostManager.cs

@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable
private const int FreeIntervalsStartCount = 25;
private readonly Stack<CostInterval> freeIntervals = new(FreeIntervalsStartCount);
private readonly Stack<CostInterval> freeIntervals = new Stack<CostInterval>(FreeIntervalsStartCount);
public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner<ushort> distArray, int pixCount, CostModel costModel)
{

16
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;
/// <summary>
/// Initializes a new instance of the <see cref="CostModel"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="literalArraySize">The literal array size.</param>
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<PixOrCopy>.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<uint> populationCounts, double[] output)
{
uint sum = 0;
int nonzeros = 0;

193
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<Vp8LHistogram> imageHisto, Vp8LHistogram tmpHisto, Span<ushort> histogramSymbols)
public static void GetHistoImageSymbols(
MemoryAllocator memoryAllocator,
int xSize,
int ySize,
Vp8LBackwardRefs refs,
uint quality,
int histoBits,
int cacheBits,
Vp8LHistogramSet imageHisto,
Vp8LHistogram tmpHisto,
Span<ushort> 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<Vp8LHistogram>(imageHistoRawSize);
for (int i = 0; i < imageHistoRawSize; i++)
{
origHisto.Add(new Vp8LHistogram(cacheBits));
}
const int entropyCombineNumBins = BinSize;
using IMemoryOwner<ushort> tmp = memoryAllocator.Allocate<ushort>(imageHistoRawSize * 2, AllocationOptions.Clean);
Span<ushort> mapTmp = tmp.Slice(0, imageHistoRawSize);
Span<ushort> 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<Vp8LHistogram> 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);
}
/// <summary>
/// Construct the histograms from the backward references.
/// </summary>
private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List<Vp8LHistogram> 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.
/// </summary>
private static void HistogramAnalyzeEntropyBin(List<Vp8LHistogram> histograms, ushort[] binMap)
private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span<ushort> binMap)
{
int histoSize = histograms.Count;
var costRange = new DominantCostRange();
DominantCostRange costRange = new();
// Analyze the dominant (literal, red and blue) entropy costs.
for (int i = 0; i < histoSize; i++)
@ -148,17 +157,20 @@ internal static class HistogramEncoder
}
}
private static int HistogramCopyAndAnalyze(List<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, Span<ushort> histogramSymbols)
private static int HistogramCopyAndAnalyze(
Vp8LHistogramSet origHistograms,
Vp8LHistogramSet histograms,
Span<ushort> 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<Vp8LHistogram> histograms,
Vp8LHistogramSet histograms,
Span<ushort> clusters,
ushort[] clusterMappings,
Span<ushort> clusterMappings,
Vp8LHistogram curCombo,
ushort[] binMap,
ReadOnlySpan<ushort> binMap,
int numBins,
double combineCostFactor)
{
@ -205,9 +217,9 @@ internal static class HistogramEncoder
clusterMappings[idx] = (ushort)idx;
}
var indicesToRemove = new List<int>();
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
List<int> 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.
/// </summary>
private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span<ushort> symbols)
private static void OptimizeHistogramSymbols(Span<ushort> clusterMappings, int numClusters, Span<ushort> clusterMappingsTmp, Span<ushort> 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.
/// </summary>
/// <returns>true if a greedy approach needs to be performed afterwards, false otherwise.</returns>
private static bool HistogramCombineStochastic(List<Vp8LHistogram> 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<HistogramPair>();
int maxSize = 9;
List<HistogramPair> histoPriorityList = new();
const int maxSize = 9;
// Fill the initial mapping.
Span<int> 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<int> src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1);
Span<int> dst = mappings.Slice(mappingIndex);
Span<int> 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<Vp8LHistogram> histograms)
private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
{
int histoSize = histograms.Count(h => h != null);
// Priority list of histogram pairs.
var histoPriorityList = new List<HistogramPair>();
List<HistogramPair> 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<Vp8LHistogram> input, List<Vp8LHistogram> output, Span<ushort> symbols)
private static void HistogramRemap(
Vp8LHistogramSet input,
Vp8LHistogramSet output,
Span<ushort> 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.
/// </summary>
/// <returns>The cost of the pair, or 0 if it superior to threshold.</returns>
private static double HistoPriorityListPush(List<HistogramPair> histoList, int maxSize, List<Vp8LHistogram> histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy)
private static double HistoPriorityListPush(
List<HistogramPair> 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
}
/// <summary>
/// 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.
/// </summary>
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;

18
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<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
public static void CreateHuffmanTree(Span<uint> histogram, int treeDepthLimit, bool[] bufRle, Span<HuffmanTree> 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.
/// </summary>
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts)
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span<uint> 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
/// <param name="histogramSize">The size of the histogram.</param>
/// <param name="treeDepthLimit">The tree depth limit.</param>
/// <param name="bitDepths">How many bits are used for the symbol.</param>
public static void GenerateOptimalTree(Span<HuffmanTree> tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
public static void GenerateOptimalTree(Span<HuffmanTree> tree, Span<uint> histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
{
uint countMin;
int treeSizeOrig = 0;
@ -177,7 +177,7 @@ internal static class HuffmanUtils
return;
}
Span<HuffmanTree> treePool = tree.Slice(treeSizeOrig);
Span<HuffmanTree> 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<HuffmanTree> treeSlice = tree.Slice(0, treeSize);
Span<HuffmanTree> 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++]

9
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;

6
src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs

@ -125,7 +125,7 @@ internal class Vp8LBitEntropy
/// <summary>
/// Get the entropy for the distribution 'X'.
/// </summary>
public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
public void BitsEntropyUnrefined(Span<uint> 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<uint> x, Span<uint> 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<uint> x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;

299
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
/// </summary>
public Vp8LHashChain HashChain { get; }
/// <summary>
/// Encodes the image as lossless webp to the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
/// Encodes the image as lossless webp to the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public void Encode<TPixel>(ImageFrame<TPixel> frame, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
/// <summary>
/// Encodes the alpha image data using the webp lossless compression.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param>
/// <returns>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.
/// </returns>
public int EncodeAlphaImageData<TPixel>(Image<TPixel> image, IMemoryOwner<byte> alphaData)
public int EncodeAlphaImageData<TPixel>(ImageFrame<TPixel> frame, IMemoryOwner<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
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.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="image">The image to encode.</param>
private void EncodeStream<TPixel>(Image<TPixel> image)
/// <param name="frame">The frame to encode.</param>
private void EncodeStream<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
int width = frame.Width;
int height = frame.Height;
Span<uint> bgra = this.Bgra.GetSpan();
Span<uint> 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="image">The image to convert.</param>
/// <param name="frame">The frame to convert.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <returns>true, if the image is non opaque.</returns>
private bool ConvertPixelsToBgra<TPixel>(Image<TPixel> image, int width, int height)
private bool ConvertPixelsToBgra<TPixel>(ImageFrame<TPixel> frame, int width, int height)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
bool nonOpaque = false;
Span<uint> bgra = this.Bgra.GetSpan();
Span<byte> bgraBytes = MemoryMarshal.Cast<uint, byte>(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<Vp8LHistogram> 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<Vp8LHistogram> 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<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{
int count = 0;
Span<int> symbols = this.scratch.Span.Slice(0, 2);
Span<int> 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<HuffmanTree> 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<ushort> histogramSymbols, HuffmanTreeCode[] huffmanCodes)
private void StoreImageToBitMask(
int width,
int histoBits,
Vp8LBackwardRefs backwardRefs,
Span<ushort> 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<HuffmanTreeCode> codes = huffmanCodes.AsSpan(5 * histogramIx);
using List<PixOrCopy>.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<Vp8LHistogram> 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<HuffmanTree> 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;
}
}
/// <summary>
@ -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);
/// <summary>
/// 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
/// </summary>
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();
}

308
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;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to create an instance from.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="basePointer">The base pointer to the backing memory.</param>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
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);
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="basePointer">The base pointer to the backing memory.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
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;
}
/// <summary>
@ -85,22 +78,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// </summary>
public double BlueCost { get; set; }
public uint[] Red { get; }
public Span<uint> Red => new(this.red, RedSize);
public uint[] Blue { get; }
public Span<uint> Blue => new(this.blue, BlueSize);
public uint[] Alpha { get; }
public Span<uint> Alpha => new(this.alpha, AlphaSize);
public uint[] Literal { get; }
public Span<uint> Distance => new(this.distance, DistanceSize);
public uint[] Distance { get; }
public Span<uint> Literal => new(this.literal, LiteralSize);
public uint TrivialSymbol { get; set; }
public bool[] IsUsed { get; }
private Span<uint> IsUsedSpan => new(this.isUsed, UsedSize);
private Span<uint> 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;
/// <summary>
/// Creates a copy of the given <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to copy to.</param>
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;
}
/// <inheritdoc/>
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;
}
/// <summary>
/// Collect all the references into a histogram (without reset).
@ -108,10 +138,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <param name="refs">The backward references.</param>
public void StoreRefs(Vp8LBackwardRefs refs)
{
using List<PixOrCopy>.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<uint> x,
Span<uint> y,
int length,
bool isXUsed,
bool isYUsed,
bool trivialAtEnd,
Vp8LStreaks stats,
Vp8LBitEntropy bitEntropy)
{
stats.Clear();
bitEntropy.Init();
@ -450,18 +482,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bitEntropy.GetEntropyUnrefined(x, length, stats);
}
}
else if (isYUsed)
{
bitEntropy.GetEntropyUnrefined(y, length, stats);
}
else
{
if (isYUsed)
{
bitEntropy.GetEntropyUnrefined(y, length, stats);
}
else
{
stats.Counts[0] = 1;
stats.Streaks[0][length > 3 ? 1 : 0] = length;
bitEntropy.Init();
}
stats.Counts[0] = 1;
stats.Streaks[0][length > 3 ? 1 : 0] = length;
bitEntropy.Init();
}
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
@ -482,7 +511,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <summary>
/// Get the symbol entropy for the distribution 'population'.
/// </summary>
private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
private double PopulationCost(Span<uint> population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
{
bitEntropy.Init();
stats.Clear();
@ -491,7 +520,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym;
// The histogram is used if there is at least one non-zero streak.
isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0;
this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0);
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
}
@ -557,3 +586,56 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
}
internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable
{
private readonly IMemoryOwner<uint> bufferOwner;
private MemoryHandle bufferHandle;
private bool isDisposed;
private OwnedVp8LHistogram(
IMemoryOwner<uint> bufferOwner,
ref MemoryHandle bufferHandle,
uint* basePointer,
int paletteCodeBits)
: base(basePointer, paletteCodeBits)
{
this.bufferOwner = bufferOwner;
this.bufferHandle = bufferHandle;
}
/// <summary>
/// Creates an <see cref="OwnedVp8LHistogram"/> that is not a member of a <see cref="Vp8LHistogramSet"/>.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits)
{
IMemoryOwner<uint> bufferOwner = memoryAllocator.Allocate<uint>(BufferSize, AllocationOptions.Clean);
MemoryHandle bufferHandle = bufferOwner.Memory.Pin();
return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits);
}
/// <summary>
/// Creates an <see cref="OwnedVp8LHistogram"/> that is not a member of a <see cref="Vp8LHistogramSet"/>.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits)
{
OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits);
histogram.StoreRefs(refs);
return histogram;
}
public void Dispose()
{
if (!this.isDisposed)
{
this.bufferHandle.Dispose();
this.bufferOwner.Dispose();
this.isDisposed = true;
}
}
}

110
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs

@ -0,0 +1,110 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Collections;
using System.Diagnostics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal sealed class Vp8LHistogramSet : IEnumerable<Vp8LHistogram>, IDisposable
{
private readonly IMemoryOwner<uint> buffer;
private MemoryHandle bufferHandle;
private readonly List<Vp8LHistogram> items;
private bool isDisposed;
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.bufferHandle = this.buffer.Memory.Pin();
unsafe
{
uint* basePointer = (uint*)this.bufferHandle.Pointer;
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits));
}
}
}
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.bufferHandle = this.buffer.Memory.Pin();
unsafe
{
uint* basePointer = (uint*)this.bufferHandle.Pointer;
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits));
}
}
}
public Vp8LHistogramSet(int capacity) => this.items = new(capacity);
public Vp8LHistogramSet() => this.items = new();
public int Count => this.items.Count;
public Vp8LHistogram this[int index]
{
get => this.items[index];
set => this.items[index] = value;
}
public void RemoveAt(int index)
{
this.CheckDisposed();
this.items.RemoveAt(index);
}
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.buffer.Dispose();
this.bufferHandle.Dispose();
this.items.Clear();
this.isDisposed = true;
}
public IEnumerator<Vp8LHistogram> GetEnumerator() => ((IEnumerable<Vp8LHistogram>)this.items).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator();
[Conditional("DEBUG")]
private void CheckDisposed()
{
if (this.isDisposed)
{
ThrowDisposed();
}
}
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet));
private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram
{
public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits)
: base(basePointer, paletteCodeBits)
{
}
public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits)
: base(basePointer, refs, paletteCodeBits)
{
}
}
}

113
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -95,12 +95,10 @@ internal sealed class WebpLosslessDecoder
public void Decode<TPixel>(Buffer2D<TPixel> pixels, int width, int height)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator))
{
this.DecodeImageStream(decoder, width, height, true);
this.DecodeImageData(decoder, decoder.Pixels.Memory.Span);
this.DecodePixelValues(decoder, pixels, width, height);
}
using Vp8LDecoder decoder = new(width, height, this.memoryAllocator);
this.DecodeImageStream(decoder, width, height, true);
this.DecodeImageData(decoder, decoder.Pixels.Memory.Span);
this.DecodePixelValues(decoder, pixels, width, height);
}
public IMemoryOwner<uint> DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0)
@ -619,12 +617,9 @@ internal sealed class WebpLosslessDecoder
Vp8LTransform transform = new(transformType, xSize, ySize);
// Each transform is allowed to be used only once.
foreach (Vp8LTransform decoderTransform in decoder.Transforms)
if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType))
{
if (decoderTransform.TransformType == transform.TransformType)
{
WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once");
}
WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once");
}
switch (transformType)
@ -744,61 +739,69 @@ internal sealed class WebpLosslessDecoder
this.bitReader.FillBitWindow();
int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]);
if (code < WebpConstants.NumLiteralCodes)
switch (code)
{
// Literal
data[pos] = (byte)code;
++pos;
++col;
if (col >= width)
case < WebpConstants.NumLiteralCodes:
{
col = 0;
++row;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
// Literal
data[pos] = (byte)code;
++pos;
++col;
if (col >= width)
{
dec.ExtractPalettedAlphaRows(row);
col = 0;
++row;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
{
dec.ExtractPalettedAlphaRows(row);
}
}
}
}
else if (code < lenCodeLimit)
{
// Backward reference
int lengthSym = code - WebpConstants.NumLiteralCodes;
int length = this.GetCopyLength(lengthSym);
int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]);
this.bitReader.FillBitWindow();
int distCode = this.GetCopyDistance(distSymbol);
int dist = PlaneCodeToDistance(width, distCode);
if (pos >= dist && end - pos >= length)
{
CopyBlock8B(data, pos, dist, length);
}
else
{
WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data");
break;
}
pos += length;
col += length;
while (col >= width)
case < lenCodeLimit:
{
col -= width;
++row;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
// Backward reference
int lengthSym = code - WebpConstants.NumLiteralCodes;
int length = this.GetCopyLength(lengthSym);
int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]);
this.bitReader.FillBitWindow();
int distCode = this.GetCopyDistance(distSymbol);
int dist = PlaneCodeToDistance(width, distCode);
if (pos >= dist && end - pos >= length)
{
dec.ExtractPalettedAlphaRows(row);
CopyBlock8B(data, pos, dist, length);
}
else
{
WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data");
}
}
if (pos < last && (col & mask) > 0)
{
htreeGroup = GetHTreeGroupForPos(hdr, col, row);
pos += length;
col += length;
while (col >= width)
{
col -= width;
++row;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
{
dec.ExtractPalettedAlphaRows(row);
}
}
if (pos < last && (col & mask) > 0)
{
htreeGroup = GetHTreeGroupForPos(hdr, col, row);
}
break;
}
}
else
{
WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data");
default:
WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data");
break;
}
this.bitReader.Eos = this.bitReader.IsEndOfStream();

11
src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs

@ -50,6 +50,11 @@ internal class Vp8EncIterator
private int uvTopIdx;
public Vp8EncIterator(Vp8Encoder enc)
: this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh)
{
}
public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh)
{
this.YTop = yTop;
@ -391,7 +396,7 @@ internal class Vp8EncIterator
this.MakeLuma16Preds();
for (mode = 0; mode < maxMode; mode++)
{
Vp8Histogram histo = new();
Vp8Histogram histo = new Vp8Histogram();
histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16);
int alpha = histo.GetAlpha();
if (alpha > bestAlpha)
@ -409,7 +414,7 @@ internal class Vp8EncIterator
{
Span<byte> modes = stackalloc byte[16];
const int maxMode = MaxIntra4Mode;
Vp8Histogram totalHisto = new();
Vp8Histogram totalHisto = new Vp8Histogram();
int curHisto = 0;
this.StartI4();
do
@ -462,7 +467,7 @@ internal class Vp8EncIterator
this.MakeChroma8Preds();
for (mode = 0; mode < maxMode; ++mode)
{
Vp8Histogram histo = new();
Vp8Histogram histo = new Vp8Histogram();
histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4);
int alpha = histo.GetAlpha();
if (alpha > bestAlpha)

153
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -4,7 +4,9 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -88,7 +90,8 @@ internal class Vp8Encoder : IDisposable
private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11;
private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const long HeaderSizeEstimate =
WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const int QMin = 0;
@ -165,7 +168,7 @@ internal class Vp8Encoder : IDisposable
// TODO: make partition_limit configurable?
const int limit = 100; // original code: limit = 100 - config->partition_limit;
this.maxI4HeaderBits =
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh];
for (int i = 0; i < this.MbInfo.Length; i++)
@ -308,27 +311,94 @@ internal class Vp8Encoder : IDisposable
/// </summary>
private int MbHeaderLimit { get; }
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)image.Width,
(uint)image.Height,
exifProfile,
xmpProfile,
metadata.IccProfile,
hasAlpha,
hasAnimation);
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
}
}
public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(frame, stream, true, null);
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeStatic<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(image.Frames.RootFrame, stream, false, image);
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
private void Encode<TPixel>(ImageFrame<TPixel> frame, Stream stream, bool hasAnimation, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
int width = frame.Width;
int height = frame.Height;
int pixelCount = width * height;
Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan();
Span<byte> v = this.V.GetSpan();
bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v);
bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v);
if (!hasAnimation)
{
this.EncodeHeader(image, stream, hasAlpha, false);
}
int yStride = width;
int uvStride = (yStride + 1) >> 1;
Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh);
Vp8EncIterator it = new(this);
Span<int> alphas = stackalloc int[WebpConstants.MaxAlpha + 1];
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
int totalMb = this.Mbw * this.Mbw;
@ -375,13 +445,6 @@ internal class Vp8Encoder : IDisposable
// Store filter stats.
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
// Extract and encode alpha channel data, if present.
int alphaDataSize = 0;
bool alphaCompressionSucceeded = false;
@ -393,7 +456,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
image,
frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@ -408,16 +471,39 @@ internal class Vp8Encoder : IDisposable
}
}
this.bitWriter.WriteEncodedImageToStream(
stream,
exifProfile,
xmpProfile,
metadata.IccProfile,
(uint)width,
(uint)height,
hasAlpha,
alphaData[..alphaDataSize],
this.alphaCompression && alphaCompressionSucceeded);
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);
}
if (hasAlpha)
{
Span<byte> data = alphaData[..alphaDataSize];
bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded;
BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed);
}
this.bitWriter.WriteEncodedImageToStream(stream);
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}
}
finally
{
@ -520,7 +606,7 @@ internal class Vp8Encoder : IDisposable
Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan();
Span<byte> v = this.V.GetSpan();
Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh);
Vp8EncIterator it = new(this);
long size = 0;
long sizeP0 = 0;
long distortion = 0;
@ -862,10 +948,11 @@ internal class Vp8Encoder : IDisposable
this.ResetSegments();
}
this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
(p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
(p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
(p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
this.SegmentHeader.Size =
(p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
(p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
(p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
(p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
}
else
{
@ -1027,7 +1114,7 @@ internal class Vp8Encoder : IDisposable
it.NzToBytes();
int pos1 = this.bitWriter.NumBytes();
int pos1 = this.bitWriter.NumBytes;
if (i16)
{
residual.Init(0, 1, this.Proba);
@ -1054,7 +1141,7 @@ internal class Vp8Encoder : IDisposable
}
}
int pos2 = this.bitWriter.NumBytes();
int pos2 = this.bitWriter.NumBytes;
// U/V
residual.Init(0, 2, this.Proba);
@ -1072,7 +1159,7 @@ internal class Vp8Encoder : IDisposable
}
}
int pos3 = this.bitWriter.NumBytes();
int pos3 = this.bitWriter.NumBytes;
it.LumaBits = pos2 - pos1;
it.UvBits = pos3 - pos2;
it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits;

187
src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs

@ -76,47 +76,48 @@ internal sealed class WebpLossyDecoder
Vp8Proba proba = new();
Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba);
using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator))
{
Vp8Io io = InitializeVp8Io(decoder, pictureHeader);
using Vp8Decoder decoder = new(
info.Vp8FrameHeader,
pictureHeader,
vp8SegmentHeader,
proba,
this.memoryAllocator);
Vp8Io io = InitializeVp8Io(decoder, pictureHeader);
// Paragraph 9.4: Parse the filter specs.
this.ParseFilterHeader(decoder);
decoder.PrecomputeFilterStrengths();
// Paragraph 9.4: Parse the filter specs.
this.ParseFilterHeader(decoder);
decoder.PrecomputeFilterStrengths();
// Paragraph 9.5: Parse partitions.
this.ParsePartitions(decoder);
// Paragraph 9.5: Parse partitions.
this.ParsePartitions(decoder);
// Paragraph 9.6: Dequantization Indices.
this.ParseDequantizationIndices(decoder);
// Paragraph 9.6: Dequantization Indices.
this.ParseDequantizationIndices(decoder);
// Ignore the value of update probabilities.
this.bitReader.ReadBool();
// Ignore the value of update probabilities.
this.bitReader.ReadBool();
// Paragraph 13.4: Parse probabilities.
this.ParseProbabilities(decoder);
// Paragraph 13.4: Parse probabilities.
this.ParseProbabilities(decoder);
// Decode image data.
this.ParseFrame(decoder, io);
// Decode image data.
this.ParseFrame(decoder, io);
if (info.Features?.Alpha == true)
{
using (AlphaDecoder alphaDecoder = new(
width,
height,
alphaData,
info.Features.AlphaChunkHeader,
this.memoryAllocator,
this.configuration))
{
alphaDecoder.Decode();
DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha);
}
}
else
{
this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
}
if (info.Features?.Alpha == true)
{
using AlphaDecoder alphaDecoder = new(
width,
height,
alphaData,
info.Features.AlphaChunkHeader,
this.memoryAllocator,
this.configuration);
alphaDecoder.Decode();
DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha);
}
else
{
this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
}
}
@ -194,8 +195,8 @@ internal sealed class WebpLossyDecoder
{
// Hardcoded tree parsing.
block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0
? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1])
: (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2);
? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1])
: (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2);
}
else
{
@ -590,57 +591,65 @@ internal sealed class WebpLossyDecoder
return;
}
if (dec.Filter == LoopFilter.Simple)
switch (dec.Filter)
{
int offset = dec.CacheYOffset + (mbx * 16);
if (mbx > 0)
case LoopFilter.Simple:
{
LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
}
int offset = dec.CacheYOffset + (mbx * 16);
if (mbx > 0)
{
LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
}
if (mby > 0)
{
LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
}
if (mby > 0)
{
LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
}
}
else if (dec.Filter == LoopFilter.Complex)
{
int uvBps = dec.CacheUvStride;
int yOffset = dec.CacheYOffset + (mbx * 16);
int uvOffset = dec.CacheUvOffset + (mbx * 8);
int hevThresh = filterInfo.HighEdgeVarianceThreshold;
if (mbx > 0)
{
LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
break;
}
if (mby > 0)
case LoopFilter.Complex:
{
LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
}
int uvBps = dec.CacheUvStride;
int yOffset = dec.CacheYOffset + (mbx * 16);
int uvOffset = dec.CacheUvOffset + (mbx * 8);
int hevThresh = filterInfo.HighEdgeVarianceThreshold;
if (mbx > 0)
{
LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
if (filterInfo.UseInnerFiltering)
{
LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
}
if (mby > 0)
{
LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
}
if (filterInfo.UseInnerFiltering)
{
LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
}
break;
}
}
}
@ -1328,18 +1337,12 @@ internal sealed class WebpLossyDecoder
private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz)
{
nzCoeffs <<= 2;
if (nz > 3)
nzCoeffs |= nz switch
{
nzCoeffs |= 3;
}
else if (nz > 1)
{
nzCoeffs |= 2;
}
else
{
nzCoeffs |= (uint)dcNz;
}
> 3 => 3,
> 1 => 2,
_ => (uint)dcNz
};
return nzCoeffs;
}
@ -1353,13 +1356,13 @@ internal sealed class WebpLossyDecoder
if (mbx == 0)
{
return mby == 0
? 6 // B_DC_PRED_NOTOPLEFT
: 5; // B_DC_PRED_NOLEFT
? 6 // B_DC_PRED_NOTOPLEFT
: 5; // B_DC_PRED_NOLEFT
}
return mby == 0
? 4 // B_DC_PRED_NOTOP
: 0; // B_DC_PRED
? 4 // B_DC_PRED_NOTOP
: 0; // B_DC_PRED
}
return mode;

6
src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs

@ -262,17 +262,17 @@ internal static class YuvConversion
/// Converts the RGB values of the image to YUV.
/// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="image">The image to convert.</param>
/// <param name="frame">The frame to convert.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="y">Span to store the luma component of the image.</param>
/// <param name="u">Span to store the u component of the image.</param>
/// <param name="v">Span to store the v component of the image.</param>
/// <returns>true, if the image contains alpha data.</returns>
public static bool ConvertRgbToYuv<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v)
public static bool ConvertRgbToYuv<TPixel>(ImageFrame<TPixel> frame, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int width = imageBuffer.Width;
int height = imageBuffer.Height;
int uvWidth = (width + 1) >> 1;

158
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -100,7 +100,7 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= 4;
switch (chunkType)
{
case WebpChunkType.Animation:
case WebpChunkType.FrameData:
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? new Color(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value;
@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
AnimationFrameData frameData = this.ReadFrameHeader(stream);
WebpFrameData frameData = WebpFrameData.Parse(stream);
long streamStartPosition = stream.Position;
Span<byte> buffer = stackalloc byte[4];
@ -162,6 +162,11 @@ internal class WebpAnimationDecoder : IDisposable
features.AlphaChunkHeader = alphaChunkHeader;
break;
case WebpChunkType.Vp8L:
if (hasAlpha)
{
WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images.");
}
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
break;
default:
@ -175,7 +180,7 @@ internal class WebpAnimationDecoder : IDisposable
{
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata);
SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration);
SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData);
imageFrame = image.Frames.RootFrame;
}
@ -183,29 +188,22 @@ internal class WebpAnimationDecoder : IDisposable
{
currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
SetFrameMetadata(currentFrame.Metadata, frameData.Duration);
SetFrameMetadata(currentFrame.Metadata, frameData);
imageFrame = currentFrame;
}
int frameX = (int)(frameData.X * 2);
int frameY = (int)(frameData.Y * 2);
int frameWidth = (int)frameData.Width;
int frameHeight = (int)frameData.Height;
Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight);
Rectangle regionRectangle = frameData.Bounds;
if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose)
if (frameData.DisposalMethod is WebpDisposalMethod.Dispose)
{
this.RestoreToBackground(imageFrame, backgroundColor);
}
using Buffer2D<TPixel> decodedImage = this.DecodeImageData<TPixel>(frameData, webpInfo);
DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending)
{
this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight);
}
bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle;
@ -217,12 +215,13 @@ internal class WebpAnimationDecoder : IDisposable
/// Sets the frames metadata.
/// </summary>
/// <param name="meta">The metadata.</param>
/// <param name="duration">The frame duration.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration)
/// <param name="frameData">The frame data.</param>
private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData)
{
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDuration = duration;
frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod;
}
/// <summary>
@ -239,7 +238,7 @@ internal class WebpAnimationDecoder : IDisposable
byte alphaChunkHeader = (byte)stream.ReadByte();
Span<byte> alphaData = this.alphaData.GetSpan();
stream.Read(alphaData, 0, alphaDataSize);
_ = stream.Read(alphaData, 0, alphaDataSize);
return alphaChunkHeader;
}
@ -251,22 +250,24 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="frameData">The frame data.</param>
/// <param name="webpInfo">The webp information.</param>
/// <returns>A decoded image.</returns>
private Buffer2D<TPixel> DecodeImageData<TPixel>(AnimationFrameData frameData, WebpImageInfo webpInfo)
private Buffer2D<TPixel> DecodeImageFrameData<TPixel>(WebpFrameData frameData, WebpImageInfo webpInfo)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> decodedImage = new((int)frameData.Width, (int)frameData.Height);
ImageFrame<TPixel> decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height);
try
{
Buffer2D<TPixel> pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> pixelBufferDecoded = decodedFrame.PixelBuffer;
if (webpInfo.IsLossless)
{
WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
WebpLosslessDecoder losslessDecoder =
new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
}
else
{
WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
WebpLossyDecoder lossyDecoder =
new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
}
@ -274,7 +275,7 @@ internal class WebpAnimationDecoder : IDisposable
}
catch
{
decodedImage?.Dispose();
decodedFrame?.Dispose();
throw;
}
finally
@ -287,48 +288,43 @@ internal class WebpAnimationDecoder : IDisposable
/// Draws the decoded image on canvas. The decoded image can be smaller the canvas.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="decodedImage">The decoded image.</param>
/// <param name="decodedImageFrame">The decoded image.</param>
/// <param name="imageFrame">The image frame to draw into.</param>
/// <param name="frameX">The frame x coordinate.</param>
/// <param name="frameY">The frame y coordinate.</param>
/// <param name="frameWidth">The width of the frame.</param>
/// <param name="frameHeight">The height of the frame.</param>
private static void DrawDecodedImageOnCanvas<TPixel>(Buffer2D<TPixel> decodedImage, ImageFrame<TPixel> imageFrame, int frameX, int frameY, int frameWidth, int frameHeight)
/// <param name="restoreArea">The area of the frame.</param>
/// <param name="blend">Whether to blend the decoded frame data onto the target frame.</param>
private static void DrawDecodedImageFrameOnCanvas<TPixel>(
Buffer2D<TPixel> decodedImageFrame,
ImageFrame<TPixel> imageFrame,
Rectangle restoreArea,
bool blend)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageFramePixels = imageFrame.PixelBuffer;
int decodedRowIdx = 0;
for (int y = frameY; y < frameY + frameHeight; y++)
// Trim the destination frame to match the restore area. The source frame is already trimmed.
Buffer2DRegion<TPixel> imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea);
if (blend)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth];
decodedPixelRow.TryCopyTo(framePixelRow[frameX..]);
// The destination frame has already been prepopulated with the pixel data from the previous frame
// so blending will leave the desired result which takes into consideration restoration to the
// background color within the restore area.
PixelBlender<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
}
return;
}
}
/// <summary>
/// After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
/// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="src">The source image.</param>
/// <param name="dst">The destination image.</param>
/// <param name="frameX">The frame x coordinate.</param>
/// <param name="frameY">The frame y coordinate.</param>
/// <param name="frameWidth">The width of the frame.</param>
/// <param name="frameHeight">The height of the frame.</param>
private void AlphaBlend<TPixel>(ImageFrame<TPixel> src, ImageFrame<TPixel> dst, int frameX, int frameY, int frameWidth, int frameHeight)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> srcPixels = src.PixelBuffer;
Buffer2D<TPixel> dstPixels = dst.PixelBuffer;
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
for (int y = frameY; y < frameY + frameHeight; y++)
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
Span<TPixel> dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
blender.Blend<TPixel>(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f);
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
decodedPixelRow.CopyTo(framePixelRow);
}
}
@ -353,42 +349,6 @@ internal class WebpAnimationDecoder : IDisposable
pixelRegion.Fill(backgroundPixel);
}
/// <summary>
/// Reads the animation frame header.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns>
private AnimationFrameData ReadFrameHeader(BufferedReadStream stream)
{
Span<byte> buffer = stackalloc byte[4];
AnimationFrameData data = new()
{
DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
// 3 bytes for the X coordinate of the upper left corner of the frame.
X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer),
// 3 bytes for the Y coordinate of the upper left corner of the frame.
Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer),
// Frame width Minus One.
Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
// Frame height Minus One.
Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
// Frame duration.
Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer)
};
byte flags = (byte)stream.ReadByte();
data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose;
data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending;
return data;
}
/// <inheritdoc/>
public void Dispose() => this.alphaData?.Dispose();
}

2
src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs → src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
internal enum AnimationBlendingMethod
public enum WebpBlendingMethod
{
/// <summary>
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.

63
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Drawing;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -77,7 +78,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
}
if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
@ -91,7 +92,7 @@ internal static class WebpChunkParsingUtils
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6);
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2));
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]);
uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7;
@ -105,23 +106,16 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
var vp8FrameHeader = new Vp8FrameHeader()
Vp8FrameHeader vp8FrameHeader = new()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
var bitReader = new Vp8BitReader(
stream,
remaining,
memoryAllocator,
partitionLength)
{
Remaining = remaining
};
Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
return new WebpImageInfo()
return new WebpImageInfo
{
Width = width,
Height = height,
@ -145,7 +139,7 @@ internal static class WebpChunkParsingUtils
// VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer);
var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
@ -174,7 +168,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
return new WebpImageInfo()
return new WebpImageInfo
{
Width = width,
Height = height,
@ -231,13 +225,13 @@ internal static class WebpChunkParsingUtils
}
// 3 bytes for the width.
uint width = ReadUnsignedInt24Bit(stream, buffer) + 1;
uint width = ReadUInt24LittleEndian(stream, buffer) + 1;
// 3 bytes for the height.
uint height = ReadUnsignedInt24Bit(stream, buffer) + 1;
uint height = ReadUInt24LittleEndian(stream, buffer) + 1;
// Read all the chunks in the order they occur.
var info = new WebpImageInfo()
WebpImageInfo info = new()
{
Width = width,
Height = height,
@ -253,7 +247,7 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param>
/// <returns>A unsigned 24 bit integer.</returns>
public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span<byte> buffer)
public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer)
{
if (stream.Read(buffer, 0, 3) == 3)
{
@ -261,7 +255,28 @@ internal static class WebpChunkParsingUtils
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
throw new ImageFormatException("Invalid Webp data, could not read unsigned integer.");
throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer.");
}
/// <summary>
/// Writes a unsigned 24 bit integer.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="data">The uint24 data to write.</param>
public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data)
{
if (data >= 1 << 24)
{
throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer.");
}
uint* ptr = &data;
byte* b = (byte*)ptr;
// Write the data in little endian.
stream.WriteByte(b[0]);
stream.WriteByte(b[1]);
stream.WriteByte(b[2]);
}
/// <summary>
@ -271,14 +286,14 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <returns>The chunk size in bytes.</returns>
public static uint ReadChunkSize(BufferedReadStream stream, Span<byte> buffer)
public static uint ReadChunkSize(Stream stream, Span<byte> buffer)
{
DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length");
DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length");
if (stream.Read(buffer) == 4)
if (stream.Read(buffer) is 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1;
}
throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
@ -298,7 +313,7 @@ internal static class WebpChunkParsingUtils
if (stream.Read(buffer) == 4)
{
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
}

11
src/ImageSharp/Formats/Webp/WebpChunkType.cs

@ -12,45 +12,54 @@ internal enum WebpChunkType : uint
/// <summary>
/// Header signaling the use of the VP8 format.
/// </summary>
/// <remarks>VP8 (Single)</remarks>
Vp8 = 0x56503820U,
/// <summary>
/// Header signaling the image uses lossless encoding.
/// </summary>
/// <remarks>VP8L (Single)</remarks>
Vp8L = 0x5650384CU,
/// <summary>
/// Header for a extended-VP8 chunk.
/// </summary>
/// <remarks>VP8X (Single)</remarks>
Vp8X = 0x56503858U,
/// <summary>
/// Chunk contains information about the alpha channel.
/// </summary>
/// <remarks>ALPH (Single)</remarks>
Alpha = 0x414C5048U,
/// <summary>
/// Chunk which contains a color profile.
/// </summary>
/// <remarks>ICCP (Single)</remarks>
Iccp = 0x49434350U,
/// <summary>
/// Chunk which contains EXIF metadata about the image.
/// </summary>
/// <remarks>EXIF (Single)</remarks>
Exif = 0x45584946U,
/// <summary>
/// Chunk contains XMP metadata about the image.
/// </summary>
/// <remarks>XMP (Single)</remarks>
Xmp = 0x584D5020U,
/// <summary>
/// For an animated image, this chunk contains the global parameters of the animation.
/// </summary>
/// <remarks>ANIM (Single)</remarks>
AnimationParameter = 0x414E494D,
/// <summary>
/// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present.
/// </summary>
Animation = 0x414E4D46,
/// <remarks>ANMF (Multiple)</remarks>
FrameData = 0x414E4D46,
}

43
src/ImageSharp/Formats/Webp/WebpConstants.cs

@ -33,39 +33,6 @@ internal static class WebpConstants
/// </summary>
public const byte Vp8LHeaderMagicByte = 0x2F;
/// <summary>
/// Signature bytes identifying a lossy image.
/// </summary>
public static readonly byte[] Vp8MagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x20 // ' '
};
/// <summary>
/// Signature bytes identifying a lossless image.
/// </summary>
public static readonly byte[] Vp8LMagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x4C // L
};
/// <summary>
/// Signature bytes identifying a VP8X header.
/// </summary>
public static readonly byte[] Vp8XMagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x58 // X
};
/// <summary>
/// The header bytes identifying RIFF file.
/// </summary>
@ -88,6 +55,11 @@ internal static class WebpConstants
0x50 // P
};
/// <summary>
/// The header bytes identifying a Webp.
/// </summary>
public const string WebpFourCc = "WEBP";
/// <summary>
/// 3 bits reserved for version.
/// </summary>
@ -103,11 +75,6 @@ internal static class WebpConstants
/// </summary>
public const int Vp8FrameHeaderSize = 10;
/// <summary>
/// Size of a VP8X chunk in bytes.
/// </summary>
public const int Vp8XChunkSize = 10;
/// <summary>
/// Size of a chunk header.
/// </summary>

9
src/ImageSharp/Formats/Webp/WebpDecoder.cs

@ -17,7 +17,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
/// <summary>
/// Gets the shared instance.
/// </summary>
public static WebpDecoder Instance { get; } = new();
public static WebpDecoder Instance { get; } = new WebpDecoder();
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
@ -25,7 +25,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options });
using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options });
return decoder.Identify(options.Configuration, stream, cancellationToken);
}
@ -35,7 +35,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
using WebpDecoderCore decoder = new(options);
using WebpDecoderCore decoder = new WebpDecoderCore(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken);
ScaleToTargetSize(options.GeneralOptions, image);
@ -52,6 +52,5 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
=> new() { GeneralOptions = options };
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options };
}

29
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -8,7 +8,9 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -89,25 +91,30 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
if (this.webImageInfo.Features is { Animation: true })
{
using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling);
using WebpAnimationDecoder animationDecoder = new(
this.memoryAllocator,
this.configuration,
this.maxFrames,
this.backgroundColorHandling);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
if (this.webImageInfo.Features is { Animation: true })
{
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
}
image = new Image<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
{
WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
WebpLosslessDecoder losslessDecoder = new(
this.webImageInfo.Vp8LBitReader,
this.memoryAllocator,
this.configuration);
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
{
WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
WebpLossyDecoder lossyDecoder = new(
this.webImageInfo.Vp8BitReader,
this.memoryAllocator,
this.configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}
@ -137,7 +144,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
metadata);
}
}
@ -332,7 +339,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return;
}
metadata.ExifProfile = new(exifData);
metadata.ExifProfile = new ExifProfile(exifData);
}
}
@ -359,7 +366,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return;
}
metadata.XmpProfile = new(xmpData);
metadata.XmpProfile = new XmpProfile(xmpData);
}
}

2
src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public sealed class WebpDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
/// <summary>
/// Gets the flag to decide how to handle the background color Animation Chunk.

2
src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs → src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
internal enum AnimationDisposalMethod
public enum WebpDisposalMethod
{
/// <summary>
/// Do not dispose. Leave the canvas as is.

2
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>

62
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless)
{
using Vp8LEncoder enc = new(
using Vp8LEncoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@ -140,11 +140,38 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.transparentColorMode,
this.nearLossless,
this.nearLosslessQuality);
enc.Encode(image, stream);
bool hasAnimation = image.Frames.Count > 1;
encoder.EncodeHeader(image, stream, hasAnimation);
if (hasAnimation)
{
foreach (ImageFrame<TPixel> imageFrame in image.Frames)
{
using Vp8LEncoder enc = new(
this.memoryAllocator,
this.configuration,
image.Width,
image.Height,
this.quality,
this.skipMetadata,
this.method,
this.transparentColorMode,
this.nearLossless,
this.nearLosslessQuality);
enc.Encode(imageFrame, stream, true);
}
}
else
{
encoder.Encode(image.Frames.RootFrame, stream, false);
}
encoder.EncodeFooter(image, stream);
}
else
{
using Vp8Encoder enc = new(
using Vp8Encoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@ -156,7 +183,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
enc.Encode(image, stream);
if (image.Frames.Count > 1)
{
encoder.EncodeHeader(image, stream, false, true);
foreach (ImageFrame<TPixel> imageFrame in image.Frames)
{
using Vp8Encoder enc = new(
this.memoryAllocator,
this.configuration,
image.Width,
image.Height,
this.quality,
this.skipMetadata,
this.method,
this.entropyPasses,
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
enc.EncodeAnimation(imageFrame, stream);
}
}
else
{
encoder.EncodeStatic(image, stream);
}
encoder.EncodeFooter(image, stream);
}
}
}

6
src/ImageSharp/Formats/Webp/WebpFormat.cs

@ -15,7 +15,7 @@ public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
/// <summary>
/// Gets the shared instance.
/// </summary>
public static WebpFormat Instance { get; } = new();
public static WebpFormat Instance { get; } = new WebpFormat();
/// <inheritdoc/>
public string Name => "Webp";
@ -30,8 +30,8 @@ public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
public IEnumerable<string> FileExtensions => WebpConstants.FileExtensions;
/// <inheritdoc/>
public WebpMetadata CreateDefaultFormatMetadata() => new();
public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
/// <inheritdoc/>
public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new();
public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata();
}

19
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -19,13 +19,28 @@ public class WebpFrameMetadata : IDeepCloneable
/// Initializes a new instance of the <see cref="WebpFrameMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration;
private WebpFrameMetadata(WebpFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
}
/// <summary>
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public WebpBlendingMethod BlendMethod { get; set; }
/// <summary>
/// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public WebpDisposalMethod DisposalMethod { get; set; }
/// <summary>
/// Gets or sets the frame duration. The time to wait before displaying the next frame,
/// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined.
/// </summary>
public uint FrameDuration { get; set; }
public uint FrameDelay { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);

9
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -23,6 +23,7 @@ public class WebpMetadata : IDeepCloneable
{
this.FileFormat = other.FileFormat;
this.AnimationLoopCount = other.AnimationLoopCount;
this.AnimationBackground = other.AnimationBackground;
}
/// <summary>
@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable
/// </summary>
public ushort AnimationLoopCount { get; set; } = 1;
/// <summary>
/// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
/// This color MAY be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the Disposal method is 1.
/// </summary>
public Color AnimationBackground { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpMetadata(this);
}

1
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Collections;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;

4
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -158,8 +158,7 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
this.Header.Size >= minSize &&
this.Header.Size < maxSize;
this.Header.Size is >= minSize and < maxSize;
}
/// <summary>
@ -175,7 +174,6 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
return copy;
}
IccWriter writer = new();
return IccWriter.Write(this);
}

17
src/ImageSharp/Primitives/Rational.cs

@ -70,7 +70,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <param name="bestPrecision">Whether to use the best possible precision when parsing the value.</param>
public Rational(double value, bool bestPrecision)
{
var rational = LongRational.FromDouble(Math.Abs(value), bestPrecision);
LongRational rational = LongRational.FromDouble(Math.Abs(value), bestPrecision);
this.Numerator = (uint)rational.Numerator;
this.Denominator = (uint)rational.Denominator;
@ -109,7 +109,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns>
/// The <see cref="Rational"/>.
/// </returns>
public static Rational FromDouble(double value) => new Rational(value, false);
public static Rational FromDouble(double value) => new(value, false);
/// <summary>
/// Converts the specified <see cref="double"/> to an instance of this type.
@ -119,24 +119,19 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns>
/// The <see cref="Rational"/>.
/// </returns>
public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision);
public static Rational FromDouble(double value, bool bestPrecision) => new(value, bestPrecision);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Rational other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(Rational other)
{
var left = new LongRational(this.Numerator, this.Denominator);
var right = new LongRational(other.Numerator, other.Denominator);
return left.Equals(right);
}
=> this.Numerator == other.Numerator && this.Denominator == other.Denominator;
/// <inheritdoc/>
public override int GetHashCode()
{
var self = new LongRational(this.Numerator, this.Denominator);
LongRational self = new(this.Numerator, this.Denominator);
return self.GetHashCode();
}
@ -169,7 +164,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns>The <see cref="string"/></returns>
public string ToString(IFormatProvider provider)
{
var rational = new LongRational(this.Numerator, this.Denominator);
LongRational rational = new(this.Numerator, this.Denominator);
return rational.ToString(provider);
}
}

13
tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs

@ -43,9 +43,9 @@ public class EncodeWebp
[Benchmark(Description = "Magick Webp Lossy")]
public void MagickWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
var defines = new WebPWriteDefines
WebPWriteDefines defines = new()
{
Lossless = false,
Method = 4,
@ -65,7 +65,7 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossy")]
public void ImageSharpWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossy,
@ -80,8 +80,8 @@ public class EncodeWebp
[Benchmark(Baseline = true, Description = "Magick Webp Lossless")]
public void MagickWebpLossless()
{
using var memoryStream = new MemoryStream();
var defines = new WebPWriteDefines
using MemoryStream memoryStream = new();
WebPWriteDefines defines = new()
{
Lossless = true,
Method = 4,
@ -97,12 +97,13 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossless")]
public void ImageSharpWebpLossless()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossless,
Method = WebpEncodingMethod.Level4,
NearLossless = false,
Quality = 75,
// This is equal to exact = false in libwebp, which is the default.
TransparentColorMode = WebpTransparentColorMode.Clear

26
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@ -79,6 +78,18 @@ public partial class PngDecoderTests
{ TestImages.Png.Rgba64Bpp, typeof(Image<Rgba64>) },
};
public static readonly string[] MultiFrameTestFiles =
{
TestImages.Png.APng,
TestImages.Png.SplitIDatZeroLength,
TestImages.Png.DisposeNone,
TestImages.Png.DisposeBackground,
TestImages.Png.DisposeBackgroundRegion,
TestImages.Png.DisposePreviousFirst,
TestImages.Png.DisposeBackgroundBeforeRegion,
TestImages.Png.BlendOverMultiple
};
[Theory]
[MemberData(nameof(PixelFormatRange))]
public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type)
@ -107,6 +118,19 @@ public partial class PngDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact);
}
[Theory]
[WithFileCollection(nameof(MultiFrameTestFiles), PixelTypes.Rgba32)]
public void Decode_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
// Some images have many frames, only compare a selection of them.
static bool Predicate(int i, int _) => i <= 8 || i % 8 == 0;
image.DebugSaveMultiFrame(provider, predicate: Predicate);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate);
}
[Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
public void PngDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider)

34
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -442,6 +442,40 @@ public partial class PngEncoderTests
});
}
[Theory]
[WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
public void Encode_APng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, PngEncoder);
memStream.Position = 0;
image.DebugSave(provider: provider, encoder: PngEncoder, null, false);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
Assert.Equal(5, image.Frames.Count);
Assert.Equal(image.Frames.Count, output.Frames.Count);
PngMetadata originalMetadata = image.Metadata.GetPngMetadata();
PngMetadata outputMetadata = output.Metadata.GetPngMetadata();
Assert.Equal(originalMetadata.RepeatCount, outputMetadata.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngFrameMetadata();
PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngFrameMetadata();
Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay);
Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod);
Assert.Equal(originalFrameMetadata.DisposalMethod, outputFrameMetadata.DisposalMethod);
}
}
[Theory]
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)

35
tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Tests.Formats.Png;
[Trait("Format", "Png")]
public class PngFrameMetadataTests
{
[Fact]
public void CloneIsDeep()
{
PngFrameMetadata meta = new()
{
FrameDelay = new(1, 0),
DisposalMethod = PngDisposalMethod.Background,
BlendMethod = PngBlendMethod.Over,
};
PngFrameMetadata clone = (PngFrameMetadata)meta.DeepClone();
Assert.True(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.True(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.True(meta.BlendMethod.Equals(clone.BlendMethod));
clone.FrameDelay = new(2, 1);
clone.DisposalMethod = PngDisposalMethod.Previous;
clone.BlendMethod = PngBlendMethod.Source;
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.False(meta.BlendMethod.Equals(clone.BlendMethod));
}
}

14
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
@ -30,15 +31,25 @@ public class PngMetadataTests
ColorType = PngColorType.GrayscaleWithAlpha,
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List<PngTextData> { new PngTextData("name", "value", "foo", "bar") }
TextData = new List<PngTextData> { new PngTextData("name", "value", "foo", "bar") },
RepeatCount = 123
};
PngMetadata clone = (PngMetadata)meta.DeepClone();
Assert.True(meta.BitDepth == clone.BitDepth);
Assert.True(meta.ColorType == clone.ColorType);
Assert.True(meta.InterlaceMethod == clone.InterlaceMethod);
Assert.True(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.True(meta.RepeatCount == clone.RepeatCount);
clone.BitDepth = PngBitDepth.Bit2;
clone.ColorType = PngColorType.Palette;
clone.InterlaceMethod = PngInterlaceMode.None;
clone.Gamma = 1;
clone.RepeatCount = 321;
Assert.False(meta.BitDepth == clone.BitDepth);
Assert.False(meta.ColorType == clone.ColorType);
@ -46,6 +57,7 @@ public class PngMetadataTests
Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.False(meta.RepeatCount == clone.RepeatCount);
}
[Theory]

2
tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.Png;

29
tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs

@ -11,7 +11,7 @@ public class DominantCostRangeTests
[Fact]
public void DominantCost_Constructor()
{
var dominantCostRange = new DominantCostRange();
DominantCostRange dominantCostRange = new();
Assert.Equal(0, dominantCostRange.LiteralMax);
Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin);
Assert.Equal(0, dominantCostRange.RedMax);
@ -24,13 +24,11 @@ public class DominantCostRangeTests
public void UpdateDominantCostRange_Works()
{
// arrange
var dominantCostRange = new DominantCostRange();
var histogram = new Vp8LHistogram(10)
{
LiteralCost = 1.0d,
RedCost = 2.0d,
BlueCost = 3.0d
};
DominantCostRange dominantCostRange = new();
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10);
histogram.LiteralCost = 1.0d;
histogram.RedCost = 2.0d;
histogram.BlueCost = 3.0d;
// act
dominantCostRange.UpdateDominantCostRange(histogram);
@ -50,7 +48,7 @@ public class DominantCostRangeTests
public void GetHistoBinIndex_Works(int partitions, int expectedIndex)
{
// arrange
var dominantCostRange = new DominantCostRange()
DominantCostRange dominantCostRange = new()
{
BlueMax = 253.4625,
BlueMin = 109.0,
@ -59,13 +57,12 @@ public class DominantCostRangeTests
RedMax = 191.0,
RedMin = 109.0
};
var histogram = new Vp8LHistogram(6)
{
LiteralCost = 247.0d,
RedCost = 112.0d,
BlueCost = 202.0d,
BitCost = 733.0d
};
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6);
histogram.LiteralCost = 247.0d;
histogram.RedCost = 112.0d;
histogram.BlueCost = 202.0d;
histogram.BitCost = 733.0d;
dominantCostRange.UpdateDominantCostRange(histogram);
// act

14
tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Webp;
@ -65,7 +66,7 @@ public class Vp8LHistogramTests
// All remaining values are expected to be zero.
literals.AsSpan().CopyTo(expectedLiterals);
var backwardRefs = new Vp8LBackwardRefs(pixelData.Length);
Vp8LBackwardRefs backwardRefs = new(pixelData.Length);
for (int i = 0; i < pixelData.Length; i++)
{
backwardRefs.Add(new PixOrCopy()
@ -76,15 +77,16 @@ public class Vp8LHistogramTests
});
}
var histogram0 = new Vp8LHistogram(backwardRefs, 3);
var histogram1 = new Vp8LHistogram(backwardRefs, 3);
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
for (int i = 0; i < 5; i++)
{
histogram0.IsUsed[i] = true;
histogram1.IsUsed[i] = true;
histogram0.IsUsed(i, true);
histogram1.IsUsed(i, true);
}
var output = new Vp8LHistogram(3);
using OwnedVp8LHistogram output = OwnedVp8LHistogram.Create(memoryAllocator, 3);
// act
histogram0.Add(histogram1, output);

14
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -308,7 +308,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@ -325,7 +325,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@ -357,6 +357,16 @@ public class WebpDecoderTests
image.CompareToOriginal(provider, ReferenceDecoder);
}
[Theory]
[WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_AlphaBlending_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance);
image.DebugSaveMultiFrame(provider);
image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact);
}
[Theory]
[WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)]
[WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)]

43
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -17,6 +17,49 @@ public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Encode_AnimatedLossless<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
WebpEncoder encoder = new()
{
FileFormat = WebpFileFormatType.Lossless,
Quality = 100
};
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "webp", encoder);
// Compare encoded result
image.VerifyEncoder(provider, "webp", string.Empty, encoder);
}
[Theory]
[WithFile(Lossy.Animated, PixelTypes.Rgba32)]
[WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
public void Encode_AnimatedLossy<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
WebpEncoder encoder = new()
{
FileFormat = WebpFileFormatType.Lossy,
Quality = 100
};
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "webp", encoder);
// Compare encoded result
// The reference decoder seems to produce differences up to 0.1% but the input/output have been
// checked to be correct.
string path = provider.Utility.GetTestOutputFileName("webp", null, true);
using Image<Rgba32> encoded = Image.Load<Rgba32>(path);
encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp");
}
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]

4
tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs

@ -143,7 +143,7 @@ public class YuvConversionTests
};
// act
YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v);
YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert
Assert.True(expectedY.AsSpan().SequenceEqual(y));
@ -249,7 +249,7 @@ public class YuvConversionTests
};
// act
YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v);
YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert
Assert.True(expectedY.AsSpan().SequenceEqual(y));

12
tests/ImageSharp.Tests/TestImages.cs

@ -62,6 +62,17 @@ public static class TestImages
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
// Animated
// https://philip.html5.org/tests/apng/tests.html
public const string APng = "Png/animated/apng.png";
public const string SplitIDatZeroLength = "Png/animated/4-split-idat-zero-length.png";
public const string DisposeNone = "Png/animated/7-dispose-none.png";
public const string DisposeBackground = "Png/animated/8-dispose-background.png";
public const string DisposeBackgroundBeforeRegion = "Png/animated/14-dispose-background-before-region.png";
public const string DisposeBackgroundRegion = "Png/animated/15-dispose-background-region.png";
public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png";
public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
public const string SubFilter3BytesPerPixel = "Png/filter1.png";
@ -671,6 +682,7 @@ public static class TestImages
public static class Lossy
{
public const string AnimatedLandscape = "Webp/landscape.webp";
public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp";
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";

28
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -179,7 +179,7 @@ public class ImagingTestCaseUtility
return path;
}
public IEnumerable<string> GetTestOutputFileNamesMultiFrame(
public IEnumerable<(int Index, string FileName)> GetTestOutputFileNamesMultiFrame(
int frameCount,
string extension = null,
object testOutputDetails = null,
@ -201,11 +201,11 @@ public class ImagingTestCaseUtility
continue;
}
yield return $"{baseDir}/{i:D2}.{extension}";
yield return (i, $"{baseDir}/{i:D2}.{extension}");
}
}
public string[] SaveTestOutputFileMultiFrame<TPixel>(
public (int Index, string FileName)[] SaveTestOutputFileMultiFrame<TPixel>(
Image<TPixel> image,
string extension = "png",
IImageEncoder encoder = null,
@ -216,27 +216,17 @@ public class ImagingTestCaseUtility
{
encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}");
string[] files = this.GetTestOutputFileNamesMultiFrame(
(int Index, string FileName)[] files = this.GetTestOutputFileNamesMultiFrame(
image.Frames.Count,
extension,
testOutputDetails,
appendPixelTypeToFileName,
predicate: predicate).ToArray();
for (int i = 0; i < image.Frames.Count; i++)
foreach ((int Index, string FileName) file in files)
{
if (predicate != null && !predicate(i, image.Frames.Count))
{
continue;
}
if (i >= files.Length)
{
break;
}
using Image<TPixel> frameImage = image.Frames.CloneFrame(i);
string filePath = files[i];
using Image<TPixel> frameImage = image.Frames.CloneFrame(file.Index);
string filePath = file.FileName;
using FileStream stream = File.OpenWrite(filePath);
frameImage.Save(stream, encoder);
}
@ -252,14 +242,14 @@ public class ImagingTestCaseUtility
=> TestEnvironment.GetReferenceOutputFileName(
this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription));
public string[] GetReferenceOutputFileNamesMultiFrame(
public (int Index, string FileName)[] GetReferenceOutputFileNamesMultiFrame(
int frameCount,
string extension,
object testOutputDetails,
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
=> this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate)
.Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
.Select(x => (x.Index, TestEnvironment.GetReferenceOutputFileName(x.FileName))).ToArray();
internal void Init(string typeName, string methodName, string outputSubfolderName)
{

19
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -336,7 +336,7 @@ public static class TestImageExtensions
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel>
{
string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame(
(int Index, string FileName)[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame(
frameCount,
extension,
testOutputDetails,
@ -345,10 +345,11 @@ public static class TestImageExtensions
List<Image<TPixel>> temporaryFrameImages = new();
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]);
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0].FileName);
foreach (string path in frameFiles)
for (int i = 0; i < frameFiles.Length; i++)
{
string path = frameFiles[i].FileName;
if (!File.Exists(path))
{
throw new FileNotFoundException("Reference output file missing: " + path);
@ -536,10 +537,8 @@ public static class TestImageExtensions
referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path);
using MemoryStream stream = new(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(referenceDecoderOptions ?? DecoderOptions.Default, stream))
{
comparer.VerifySimilarity(original, image);
}
using Image<TPixel> original = referenceDecoder.Decode<TPixel>(referenceDecoderOptions ?? DecoderOptions.Default, stream);
comparer.VerifySimilarity(original, image);
return image;
}
@ -562,10 +561,8 @@ public static class TestImageExtensions
referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path);
using MemoryStream stream = new(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream))
{
comparer.VerifySimilarity(original, image);
}
using Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream);
comparer.VerifySimilarity(original, image);
return image;
}

8
tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs

@ -200,13 +200,13 @@ public class TestImageProviderTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image);
(int Index, string FileName)[] files = provider.Utility.SaveTestOutputFileMultiFrame(image);
Assert.True(files.Length > 2);
foreach (string path in files)
foreach ((int Index, string FileName) file in files)
{
this.Output.WriteLine(path);
Assert.True(File.Exists(path));
this.Output.WriteLine(file.FileName);
Assert.True(File.Exists(file.FileName));
}
}

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556
size 102

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556
size 102

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38
size 99

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38
size 99

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1322aa335ad845cacfa20266bc0ffc31db117376373c15bcdb222abcf4b8f83
size 113

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a
size 89

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save