Browse Source

Merge branch 'main' into animated-webp-encoder

pull/2569/head
Poker 2 years ago
committed by GitHub
parent
commit
5737e4afea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  2. 47
      src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
  3. 160
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  4. 2
      src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
  5. 14
      src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
  6. 2
      src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
  7. 20
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  8. 22
      src/ImageSharp/Formats/Png/PngBlendMethod.cs
  9. 7
      src/ImageSharp/Formats/Png/PngChunk.cs
  10. 65
      src/ImageSharp/Formats/Png/PngChunkType.cs
  11. 8
      src/ImageSharp/Formats/Png/PngConstants.cs
  12. 468
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  13. 25
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  14. 446
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  15. 5
      src/ImageSharp/Formats/Png/PngFormat.cs
  16. 62
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  17. 9
      src/ImageSharp/Formats/Png/PngMetadata.cs
  18. 359
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  19. 26
      src/ImageSharp/Formats/Png/PngThrowHelper.cs
  20. 1
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  21. 17
      src/ImageSharp/Primitives/Rational.cs
  22. 26
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  23. 34
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  24. 35
      tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
  25. 14
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  26. 2
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
  27. 11
      tests/ImageSharp.Tests/TestImages.cs
  28. 28
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  29. 19
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  30. 8
      tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs
  31. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png
  32. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png
  33. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png
  34. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png
  35. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png
  36. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png
  37. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png
  38. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png
  39. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png
  40. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png
  41. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png
  42. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png
  43. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png
  44. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png
  45. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png
  46. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png
  47. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png
  48. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png
  49. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png
  50. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png
  51. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png
  52. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png
  53. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png
  54. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png
  55. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png
  56. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png
  57. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png
  58. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png
  59. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png
  60. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png
  61. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png
  62. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png
  63. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png
  64. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png
  65. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png
  66. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png
  67. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png
  68. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png
  69. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png
  70. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png
  71. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png
  72. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png
  73. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png
  74. 3
      tests/Images/Input/Png/animated/12-dispose-prev-first.png
  75. 3
      tests/Images/Input/Png/animated/14-dispose-background-before-region.png
  76. 3
      tests/Images/Input/Png/animated/15-dispose-background-region.png
  77. 3
      tests/Images/Input/Png/animated/21-blend-over-multiple.png
  78. 3
      tests/Images/Input/Png/animated/4-split-idat-zero-length.png
  79. 3
      tests/Images/Input/Png/animated/7-dispose-none.png
  80. 3
      tests/Images/Input/Png/animated/8-dispose-background.png
  81. 3
      tests/Images/Input/Png/animated/apng.png

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

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;

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

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;

11
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";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png

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

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

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

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

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

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

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png

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

3
tests/Images/Input/Png/animated/12-dispose-prev-first.png

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

3
tests/Images/Input/Png/animated/14-dispose-background-before-region.png

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

3
tests/Images/Input/Png/animated/15-dispose-background-region.png

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

3
tests/Images/Input/Png/animated/21-blend-over-multiple.png

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

3
tests/Images/Input/Png/animated/4-split-idat-zero-length.png

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

3
tests/Images/Input/Png/animated/7-dispose-none.png

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

3
tests/Images/Input/Png/animated/8-dispose-background.png

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

3
tests/Images/Input/Png/animated/apng.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c15e4670da1826d1cc25555bd6cbe287ecc70327cd029a7613334a39a283021
size 2508
Loading…
Cancel
Save