From 1c474c7758c3a192aa53aef557ba0ff5f9b4f79d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 17:19:31 +0200 Subject: [PATCH 1/8] Add decoding color profile chunk --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 12770bc521..3dc6d0bf42 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -19,6 +19,7 @@ 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken 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,58 @@ private bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string data) 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 < PngConstants.MinTextKeywordLength || zeroIndex > 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); + + using (var memoryStream = new MemoryStream(compressedData.ToArray())) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) + { + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + return; + } + + 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); + } + + byte[] iccpProfileBytes = uncompressedBytes.ToArray(); + metadata.IccProfile = new IccProfile(iccpProfileBytes); + } + } + /// /// Compares two ReadOnlySpan<char>s in a case-insensitive method. /// This is only needed because older frameworks are missing the extension method. From 9b5d56f585adb4e032fc41ef37f27097b2526134 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 18:53:57 +0200 Subject: [PATCH 2/8] Preserve color profile when encoding png's --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 70 ++++++++++++++------ 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c443c0fcf1..8663915279 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -87,6 +87,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// private IMemoryOwner currentScanline; + /// + /// The color profile name. + /// + private const string ProfileName = "ICC Profile"; + /// /// Initializes a new instance of the class. /// @@ -134,6 +139,7 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) } /// - /// 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 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta) return; } - var xmpData = meta.XmpProfile.Data; + byte[] xmpData = meta.XmpProfile.Data; if (xmpData.Length == 0) { @@ -687,19 +693,48 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta) PngConstants.XmpKeyword.CopyTo(payload); int bytesWritten = PngConstants.XmpKeyword.Length; - // Write the iTxt header (all zeros in this case) + // Write the iTxt header (all zeros in this case). payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; payload[bytesWritten++] = 0; - // 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; + } + + string profileName = "ICC Profile"; + byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); + + byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); + int payloadLength = profileName.Length + compressedData.Length + 2; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(profileName).CopyTo(outputBytes); + int bytesWritten = profileName.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 +762,12 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) } } - 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 +806,17 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) 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 +829,8 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) 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 +840,15 @@ private void WriteTextChunks(Stream stream, PngMetadata meta) /// /// 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(); From d645ba4bf3ff7ba9e26b334897852b92d13311d0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 19:08:31 +0200 Subject: [PATCH 3/8] Add test for ICC profile --- .../Formats/Png/PngEncoderTests.Chunks.cs | 1 + .../Formats/Png/PngMetadataTests.cs | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) 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 @@ public void ExcludeFilter_WithNone_DoesNotExcludeChunks() { 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 @@ public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolutio } } + [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) From 35d1473d7adc7fa16cad589e73da55e294edc35e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 19:11:23 +0200 Subject: [PATCH 4/8] Use const color profile name --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8663915279..e5b49c41d3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -90,7 +90,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The color profile name. /// - private const string ProfileName = "ICC Profile"; + private const string ColorProfileName = "ICC Profile"; /// /// Initializes a new instance of the class. @@ -718,16 +718,15 @@ private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData) return; } - string profileName = "ICC Profile"; byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); - int payloadLength = profileName.Length + compressedData.Length + 2; + int payloadLength = ColorProfileName.Length + compressedData.Length + 2; using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { Span outputBytes = owner.GetSpan(); - PngConstants.Encoding.GetBytes(profileName).CopyTo(outputBytes); - int bytesWritten = profileName.Length; + PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes); + int bytesWritten = ColorProfileName.Length; outputBytes[bytesWritten++] = 0; // Null separator. outputBytes[bytesWritten++] = 0; // Compression. compressedData.CopyTo(outputBytes.Slice(bytesWritten)); From b025d29de4e6ba350275f122038f0fdabfcf2b09 Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Tue, 10 May 2022 09:30:34 +0200 Subject: [PATCH 5/8] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3dc6d0bf42..0745a5f5a5 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1186,7 +1186,7 @@ private bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string data) private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data) { int zeroIndex = data.IndexOf((byte)0); - if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) + if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) { return; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e5b49c41d3..ad16c80374 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -694,11 +694,13 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta) 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; + 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. xmpData.CopyTo(payload.Slice(bytesWritten)); From 2dd35982e7efd4a52dba1f3f6cd7b1b2b4de96a9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 10 May 2022 09:47:09 +0200 Subject: [PATCH 6/8] Avoid allocation, remove code duplication for decompressing zlib data --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 77 ++++++++++---------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 0745a5f5a5..470a245b24 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1206,27 +1206,45 @@ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan da ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - 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[] iccpProfileBytes)) { - if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + 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 (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) { - return; - } + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + uncompressedBytesArray = Array.Empty(); + return false; + } - var uncompressedBytes = new List(); + var uncompressedBytes = new List(compressedData.Length); - // 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); - } + // 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); + } - byte[] iccpProfileBytes = uncompressedBytes.ToArray(); - metadata.IccProfile = new IccProfile(iccpProfileBytes); + uncompressedBytesArray = uncompressedBytes.ToArray(); + return true; + } } } @@ -1362,7 +1380,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpanThe . 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; } /// From d43ec499e3cdfe1e0d100e12c781360471e9bc69 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 May 2022 14:32:00 +0200 Subject: [PATCH 7/8] Use memory allocator for destination buffer for the uncomressed bytes --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 470a245b24..16971b5b86 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1222,10 +1222,12 @@ private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out { fixed (byte* compressedDataBase = compressedData) { + using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize)) using (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) using (var inflateStream = new ZlibInflateStream(bufferedStream)) { + Span dest = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { uncompressedBytesArray = Array.Empty(); @@ -1233,13 +1235,11 @@ private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out } var uncompressedBytes = new List(compressedData.Length); - - // 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); + int bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); while (bytesRead != 0) { - uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + uncompressedBytes.AddRange(dest.Slice(0, bytesRead).ToArray()); + bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); } uncompressedBytesArray = uncompressedBytes.ToArray(); From a0e38c87b01af9c47101947580870f09513ed151 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 May 2022 14:59:50 +0200 Subject: [PATCH 8/8] Use memory stream for uncompressed data instead of a list --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 16971b5b86..f46b5058a1 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1223,26 +1223,26 @@ private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out fixed (byte* compressedDataBase = compressedData) { using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize)) - using (var memoryStream = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) - using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) + 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 dest = destBuffer.GetSpan(); + Span destUncompressedData = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { uncompressedBytesArray = Array.Empty(); return false; } - var uncompressedBytes = new List(compressedData.Length); - int bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); while (bytesRead != 0) { - uncompressedBytes.AddRange(dest.Slice(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(dest, 0, dest.Length); + memoryStreamOutput.Write(destUncompressedData.Slice(0, bytesRead)); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); } - uncompressedBytesArray = uncompressedBytes.ToArray(); + uncompressedBytesArray = memoryStreamOutput.ToArray(); return true; } }