diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 0247dba351..a5bcff3b2b 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -272,6 +272,21 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
case PngChunkType.Text:
this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ break;
+ case PngChunkType.CompressedText:
+ this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ break;
+ case PngChunkType.InternationalText:
+ this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
+ break;
+ case PngChunkType.Exif:
+ if (!this.ignoreMetadata)
+ {
+ var exifData = new byte[chunk.Length];
+ Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length);
+ metadata.ExifProfile = new ExifProfile(exifData);
+ }
+
break;
case PngChunkType.End:
this.isEndChunkReached = true;
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index fcbbc66974..34d5ee7739 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 5a31d2d93c..fb5dc9a634 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/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index 5f5d5fd3d7..bf42066002 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@@ -56,17 +57,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (Image image = provider.GetImage(new PngDecoder()))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'") && m.LanguageTag.Equals("x-klingon") && m.TranslatedKeyword.Equals("warning"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort"));
+ VerifyTextDataIsPresent(meta);
}
}
@@ -85,17 +76,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (Image image = decoder.Decode(Configuration.Default, memoryStream))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'") && m.LanguageTag.Equals("x-klingon") && m.TranslatedKeyword.Equals("warning"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag"));
- Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort"));
+ VerifyTextDataIsPresent(meta);
}
}
}
@@ -149,6 +130,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
+ [Theory]
+ [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
+ public void Decode_ReadsExifData(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ var decoder = new PngDecoder
+ {
+ IgnoreMetadata = false
+ };
+
+ using (Image image = provider.GetImage(decoder))
+ {
+ Assert.NotNull(image.Metadata.ExifProfile);
+ ExifProfile exif = image.Metadata.ExifProfile;
+ VerifyExifDataIsPresent(exif);
+ }
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
+ public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ var decoder = new PngDecoder
+ {
+ IgnoreMetadata = true
+ };
+
+ using (Image image = provider.GetImage(decoder))
+ {
+ Assert.Null(image.Metadata.ExifProfile);
+ }
+ }
+
[Fact]
public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead()
{
@@ -178,7 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
IgnoreMetadata = true
};
- var testFile = TestFile.Create(TestImages.Png.Blur);
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using (Image image = testFile.CreateRgba32Image(options))
{
@@ -220,5 +235,61 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
+
+ [Theory]
+ [InlineData(TestImages.Png.PngWithMetadata)]
+ public void Identify_ReadsTextData(string imagePath)
+ {
+ var testFile = TestFile.Create(imagePath);
+ using (var stream = new MemoryStream(testFile.Bytes, false))
+ {
+ IImageInfo imageInfo = Image.Identify(stream);
+ Assert.NotNull(imageInfo);
+ PngMetadata meta = imageInfo.Metadata.GetFormatMetadata(PngFormat.Instance);
+ VerifyTextDataIsPresent(meta);
+ }
+ }
+
+ [Theory]
+ [InlineData(TestImages.Png.PngWithMetadata)]
+ public void Identify_ReadsExifData(string imagePath)
+ {
+ var testFile = TestFile.Create(imagePath);
+ using (var stream = new MemoryStream(testFile.Bytes, false))
+ {
+ IImageInfo imageInfo = Image.Identify(stream);
+ Assert.NotNull(imageInfo);
+ Assert.NotNull(imageInfo.Metadata.ExifProfile);
+ ExifProfile exif = imageInfo.Metadata.ExifProfile;
+ VerifyExifDataIsPresent(exif);
+ }
+ }
+
+ private static void VerifyExifDataIsPresent(ExifProfile exif)
+ {
+ Assert.Equal(1, exif.Values.Count);
+ IExifValue software = exif.GetValue(ExifTag.Software);
+ Assert.NotNull(software);
+ Assert.Equal("ImageSharp", software.Value);
+ }
+
+ private static void VerifyTextDataIsPresent(PngMetadata meta)
+ {
+ Assert.NotNull(meta);
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'") &&
+ m.LanguageTag.Equals("x-klingon") && m.TranslatedKeyword.Equals("warning"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") &&
+ m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") &&
+ m.LanguageTag.Equals("chinese"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag"));
+ Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort"));
+ }
}
}
diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png
index 54c08ca42c..8db95fa632 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:c0490f627b22a3487b78e2797ebb65f5741fdbabfd4a3d9db806ca624f62fe8c
-size 805
+oid sha256:a37d2d31c2148b94bfd732c8964808dcc2dcdb6d2c187bb5d0403dc09af9ab46
+size 60544