diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 12770bc521..f46b5058a1 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -19,6 +19,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
@@ -205,6 +206,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true);
}
+ break;
+ case PngChunkType.EmbeddedColorProfile:
+ this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.End:
goto EOF;
@@ -1174,6 +1178,76 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
+ ///
+ /// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
+ ///
+ /// The metadata.
+ /// The bytes containing the profile.
+ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data)
+ {
+ int zeroIndex = data.IndexOf((byte)0);
+ if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength)
+ {
+ return;
+ }
+
+ byte compressionMethod = data[zeroIndex + 1];
+ if (compressionMethod != 0)
+ {
+ // Only compression method 0 is supported (zlib datastream with deflate compression).
+ return;
+ }
+
+ ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex);
+ if (!this.TryReadTextKeyword(keywordBytes, out string name))
+ {
+ return;
+ }
+
+ ReadOnlySpan compressedData = data.Slice(zeroIndex + 2);
+
+ if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes))
+ {
+ metadata.IccProfile = new IccProfile(iccpProfileBytes);
+ }
+ }
+
+ ///
+ /// Tries to un-compress zlib compressed data.
+ ///
+ /// The compressed data.
+ /// The uncompressed bytes array.
+ /// True, if de-compressing was successful.
+ private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out byte[] uncompressedBytesArray)
+ {
+ fixed (byte* compressedDataBase = compressedData)
+ {
+ using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize))
+ using (var memoryStreamOutput = new MemoryStream(compressedData.Length))
+ using (var memoryStreamInput = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length))
+ using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStreamInput))
+ using (var inflateStream = new ZlibInflateStream(bufferedStream))
+ {
+ Span destUncompressedData = destBuffer.GetSpan();
+ if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
+ {
+ uncompressedBytesArray = Array.Empty();
+ return false;
+ }
+
+ int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
+ while (bytesRead != 0)
+ {
+ memoryStreamOutput.Write(destUncompressedData.Slice(0, bytesRead));
+ bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
+ }
+
+ uncompressedBytesArray = memoryStreamOutput.ToArray();
+ return true;
+ }
+ }
+ }
+
///
/// Compares two ReadOnlySpan<char>s in a case-insensitive method.
/// This is only needed because older frameworks are missing the extension method.
@@ -1306,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else if (this.IsXmpTextData(keywordBytes))
{
- XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
+ var xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
metadata.XmpProfile = xmpProfile;
}
else
@@ -1325,29 +1399,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The .
private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value)
{
- using (var memoryStream = new MemoryStream(compressedData.ToArray()))
- using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream))
- using (var inflateStream = new ZlibInflateStream(bufferedStream))
+ if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
{
- if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
- {
- value = null;
- return false;
- }
-
- var uncompressedBytes = new List();
-
- // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here.
- int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
- while (bytesRead != 0)
- {
- uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray());
- bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
- }
-
- value = encoding.GetString(uncompressedBytes.ToArray());
+ value = encoding.GetString(uncompressedData);
return true;
}
+
+ value = null;
+ return false;
}
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index c443c0fcf1..ad16c80374 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -87,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Png
///
private IMemoryOwner currentScanline;
+ ///
+ /// The color profile name.
+ ///
+ private const string ColorProfileName = "ICC Profile";
+
///
/// Initializes a new instance of the class.
///
@@ -134,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
+ this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
@@ -656,7 +662,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
///
- /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
+ /// Writes an iTXT chunk, containing the XMP metadata to the stream, if such profile is present in the metadata.
///
/// The containing image data.
/// The image metadata.
@@ -673,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Png
return;
}
- var xmpData = meta.XmpProfile.Data;
+ byte[] xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0)
{
@@ -687,19 +693,49 @@ namespace SixLabors.ImageSharp.Formats.Png
PngConstants.XmpKeyword.CopyTo(payload);
int bytesWritten = PngConstants.XmpKeyword.Length;
- // Write the iTxt header (all zeros in this case)
- payload[bytesWritten++] = 0;
- payload[bytesWritten++] = 0;
- payload[bytesWritten++] = 0;
- payload[bytesWritten++] = 0;
- payload[bytesWritten++] = 0;
+ // Write the iTxt header (all zeros in this case).
+ Span iTxtHeader = payload.Slice(bytesWritten);
+ iTxtHeader[4] = 0;
+ iTxtHeader[3] = 0;
+ iTxtHeader[2] = 0;
+ iTxtHeader[1] = 0;
+ iTxtHeader[0] = 0;
+ bytesWritten += 5;
- // And the XMP data itself
+ // And the XMP data itself.
xmpData.CopyTo(payload.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
}
+ ///
+ /// Writes the color profile chunk.
+ ///
+ /// The stream to write to.
+ /// The image meta data.
+ private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData)
+ {
+ if (metaData.IccProfile is null)
+ {
+ return;
+ }
+
+ byte[] iccProfileBytes = metaData.IccProfile.ToByteArray();
+
+ byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes);
+ int payloadLength = ColorProfileName.Length + compressedData.Length + 2;
+ using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength))
+ {
+ Span outputBytes = owner.GetSpan();
+ PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes);
+ int bytesWritten = ColorProfileName.Length;
+ outputBytes[bytesWritten++] = 0; // Null separator.
+ outputBytes[bytesWritten++] = 0; // Compression.
+ compressedData.CopyTo(outputBytes.Slice(bytesWritten));
+ this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes);
+ }
+ }
+
///
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.
@@ -727,13 +763,12 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
- if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) ||
- !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
+ if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold
- ? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
+ ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
@@ -772,18 +807,17 @@ namespace SixLabors.ImageSharp.Formats.Png
if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Write zTXt chunk.
- byte[] compressedData =
- this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
+ byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value));
int payloadLength = textData.Keyword.Length + compressedData.Length + 2;
using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength))
{
Span outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
- outputBytes[bytesWritten++] = 0;
- outputBytes[bytesWritten++] = 0;
+ outputBytes[bytesWritten++] = 0; // Null separator.
+ outputBytes[bytesWritten++] = 0; // Compression.
compressedData.CopyTo(outputBytes.Slice(bytesWritten));
- this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray());
+ this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
}
}
else
@@ -796,9 +830,8 @@ namespace SixLabors.ImageSharp.Formats.Png
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
- PngConstants.Encoding.GetBytes(textData.Value)
- .CopyTo(outputBytes.Slice(bytesWritten));
- this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
+ PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(bytesWritten));
+ this.WriteChunk(stream, PngChunkType.Text, outputBytes);
}
}
}
@@ -808,15 +841,15 @@ namespace SixLabors.ImageSharp.Formats.Png
///
/// Compresses a given text using Zlib compression.
///
- /// The text bytes to compress.
- /// The compressed text byte array.
- private byte[] GetCompressedTextBytes(byte[] textBytes)
+ /// The bytes to compress.
+ /// The compressed byte array.
+ private byte[] GetZlibCompressedBytes(byte[] dataBytes)
{
using (var memoryStream = new MemoryStream())
{
using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
{
- deflateStream.Write(textBytes);
+ deflateStream.Write(dataBytes);
}
return memoryStream.ToArray();
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
index 30a6847029..ede4ec1ccc 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
@@ -302,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
PngChunkType.Header,
PngChunkType.Gamma,
+ PngChunkType.EmbeddedColorProfile,
PngChunkType.Palette,
PngChunkType.InternationalText,
PngChunkType.Text,
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index fd39a828f9..d0ae61fd4c 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public class PngMetadataTests
{
public static readonly TheoryData RatioFiles =
- new TheoryData
+ new()
{
{ TestImages.Png.Splash, 11810, 11810, PixelResolutionUnit.PixelsPerMeter },
{ TestImages.Png.Ratio1x4, 1, 4, PixelResolutionUnit.AspectRatio },
@@ -222,6 +222,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
+ [Theory]
+ [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
+ public void Encode_PreservesColorProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image input = provider.GetImage(new PngDecoder()))
+ {
+ ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
+ byte[] expectedProfileBytes = expectedProfile.ToByteArray();
+
+ using (var memStream = new MemoryStream())
+ {
+ input.Save(memStream, new PngEncoder());
+
+ memStream.Position = 0;
+ using (var output = Image.Load(memStream))
+ {
+ ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
+ byte[] actualProfileBytes = actualProfile.ToByteArray();
+
+ Assert.NotNull(actualProfile);
+ Assert.Equal(expectedProfileBytes, actualProfileBytes);
+ }
+ }
+ }
+ }
+
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)