diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index fcbbc6697..34d5ee773 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -149,10 +149,10 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream);
+ this.WriteGammaChunk(stream);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
- this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
@@ -538,6 +538,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Writes the palette chunk to the stream.
+ /// Should be written before the first IDAT chunk.
///
/// The pixel format.
/// The containing image data.
@@ -595,6 +596,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Writes the physical dimension information to the stream.
+ /// Should be written before IDAT chunk.
///
/// The containing image data.
/// The image metadata.
@@ -716,6 +718,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Writes the gamma information to the stream.
+ /// Should be written before PLTE and IDAT chunk.
///
/// The containing image data.
private void WriteGammaChunk(Stream stream)
@@ -733,6 +736,7 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Writes the transparency chunk to the stream.
+ /// Should be written after PLTE and before IDAT.
///
/// The containing image data.
/// The image metadata.
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 5a31d2d93..fb5dc9a63 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0.
// ReSharper disable InconsistentNaming
+using System;
+using System.Buffers.Binary;
using System.IO;
using System.Linq;
@@ -18,6 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngEncoderTests
{
+ private static PngEncoder PngEncoder => new PngEncoder();
+
public static readonly TheoryData PngBitDepthFiles =
new TheoryData
{
@@ -234,8 +238,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
using (Stream stream = new MemoryStream())
{
- var encoder = new PngEncoder();
- encoder.Encode(provider.GetImage(), stream);
+ PngEncoder.Encode(provider.GetImage(), stream);
stream.Seek(0, SeekOrigin.Begin);
@@ -281,7 +284,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (Image image = provider.GetImage())
using (var ms = new MemoryStream())
{
- image.Save(ms, new PngEncoder());
+ image.Save(ms, PngEncoder);
byte[] data = ms.ToArray().Take(8).ToArray();
byte[] expected =
@@ -304,14 +307,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
- var options = new PngEncoder();
-
var testFile = TestFile.Create(imagePath);
using (Image input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
- input.Save(memStream, options);
+ input.Save(memStream, PngEncoder);
memStream.Position = 0;
using (var output = Image.Load(memStream))
@@ -329,14 +330,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[MemberData(nameof(PngBitDepthFiles))]
public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
{
- var options = new PngEncoder();
-
var testFile = TestFile.Create(imagePath);
using (Image input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
- input.Save(memStream, options);
+ input.Save(memStream, PngEncoder);
memStream.Position = 0;
using (var output = Image.Load(memStream))
@@ -353,8 +352,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
{
- var options = new PngEncoder();
-
var testFile = TestFile.Create(imagePath);
using (Image input = testFile.CreateRgba32Image())
{
@@ -363,7 +360,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (var memStream = new MemoryStream())
{
- input.Save(memStream, options);
+ input.Save(memStream, PngEncoder);
memStream.Position = 0;
using (var output = Image.Load(memStream))
{
@@ -404,6 +401,126 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
+ [Fact]
+ public void HeaderChunk_ComesFirst()
+ {
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ input.Save(memStream, PngEncoder);
+ memStream.Position = 0;
+
+ // Skip header.
+ Span bytesSpan = memStream.ToArray().AsSpan(8);
+ BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.Equal(PngChunkType.Header, type);
+ }
+
+ [Fact]
+ public void EndChunk_IsLast()
+ {
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ input.Save(memStream, PngEncoder);
+ memStream.Position = 0;
+
+ // Skip header.
+ Span bytesSpan = memStream.ToArray().AsSpan(8);
+
+ bool endChunkFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.False(endChunkFound);
+ if (type == PngChunkType.End)
+ {
+ endChunkFound = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
+ [Theory]
+ [InlineData(PngChunkType.Gamma)]
+ [InlineData(PngChunkType.Chroma)]
+ [InlineData(PngChunkType.EmbeddedColorProfile)]
+ [InlineData(PngChunkType.SignificantBits)]
+ [InlineData(PngChunkType.StandardRgbColourSpace)]
+ public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
+ {
+ var chunkType = (PngChunkType)chunkTypeObj;
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ input.Save(memStream, PngEncoder);
+ memStream.Position = 0;
+
+ // Skip header.
+ Span bytesSpan = memStream.ToArray().AsSpan(8);
+
+ bool palFound = false;
+ bool dataFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ if (chunkType == type)
+ {
+ Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
+ }
+
+ switch (type)
+ {
+ case PngChunkType.Data:
+ dataFound = true;
+ break;
+ case PngChunkType.Palette:
+ palFound = true;
+ break;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
+ [Theory]
+ [InlineData(PngChunkType.Physical)]
+ [InlineData(PngChunkType.SuggestedPalette)]
+ public void Chunk_ComesBeforeIDat(object chunkTypeObj)
+ {
+ var chunkType = (PngChunkType)chunkTypeObj;
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ input.Save(memStream, PngEncoder);
+ memStream.Position = 0;
+
+ // Skip header.
+ Span bytesSpan = memStream.ToArray().AsSpan(8);
+
+ bool dataFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ if (chunkType == type)
+ {
+ Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
+ }
+
+ if (type == PngChunkType.Data)
+ {
+ dataFound = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
[Theory]
[WithTestPatternImages(587, 821, PixelTypes.Rgba32)]
[WithTestPatternImages(677, 683, PixelTypes.Rgba32)]
diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png
index 16be2b0e8..8db95fa63 100644
--- a/tests/Images/Input/Png/PngWithMetaData.png
+++ b/tests/Images/Input/Png/PngWithMetaData.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c92a4ea43f50a7b5f5a492991c0a619ab639c4e802bf4ae0c2433a066e8c05a7
-size 777
+oid sha256:a37d2d31c2148b94bfd732c8964808dcc2dcdb6d2c187bb5d0403dc09af9ab46
+size 60544