From b099bda98b5011428899ea6a29f5ec3ca4e51562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:35:16 +0100 Subject: [PATCH 01/10] Reduced intermediate allocations: Tiff --- .../BlackIsZero32FloatTiffColor{TPixel}.cs | 11 +++-- .../RgbFloat323232TiffColor{TPixel}.cs | 23 +++++----- .../RgbaFloat32323232TiffColor{TPixel}.cs | 30 ++++++------- .../WhiteIsZero32FloatTiffColor{TPixel}.cs | 9 ++-- .../Formats/Tiff/TiffEncoderCore.cs | 38 ++++++++-------- .../Formats/Tiff/Writers/TiffStreamWriter.cs | 44 +++++++++---------- .../Formats/Tiff/BigTiffMetadataTests.cs | 4 +- .../Formats/Tiff/TiffEncoderHeaderTests.cs | 4 +- .../Formats/Tiff/Utils/TiffWriterTests.cs | 15 ++++--- 9 files changed, 85 insertions(+), 93 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs index 9007b3f5ab..df37327c35 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -24,9 +24,9 @@ internal class BlackIsZero32FloatTiffColor : TiffBaseColorDecoder public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { - var color = default(TPixel); + TPixel color = default; color.FromScaledVector4(Vector4.Zero); - byte[] buffer = new byte[4]; + Span buffer = stackalloc byte[4]; int offset = 0; for (int y = top; y < top + height; y++) @@ -37,8 +37,8 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float intensity = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float intensity = BitConverter.ToSingle(buffer); offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); @@ -50,8 +50,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 4).CopyTo(buffer); - float intensity = BitConverter.ToSingle(buffer, 0); + float intensity = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs index 31a53b65d0..1c3af55621 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -27,7 +27,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in var color = default(TPixel); color.FromScaledVector4(Vector4.Zero); int offset = 0; - byte[] buffer = new byte[4]; + Span buffer = stackalloc byte[4]; for (int y = top; y < top + height; y++) { @@ -38,18 +38,18 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float r = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float r = BitConverter.ToSingle(buffer); offset += 4; data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float g = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float g = BitConverter.ToSingle(buffer); offset += 4; data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float b = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float b = BitConverter.ToSingle(buffer); offset += 4; var colorVector = new Vector4(r, g, b, 1.0f); @@ -61,16 +61,13 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 4).CopyTo(buffer); - float r = BitConverter.ToSingle(buffer, 0); + float r = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - data.Slice(offset, 4).CopyTo(buffer); - float g = BitConverter.ToSingle(buffer, 0); + float g = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - data.Slice(offset, 4).CopyTo(buffer); - float b = BitConverter.ToSingle(buffer, 0); + float b = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; var colorVector = new Vector4(r, g, b, 1.0f); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs index 920f9fdc43..743502d56e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs @@ -27,7 +27,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in var color = default(TPixel); color.FromScaledVector4(Vector4.Zero); int offset = 0; - byte[] buffer = new byte[4]; + Span buffer = stackalloc byte[4]; for (int y = top; y < top + height; y++) { @@ -38,23 +38,23 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float r = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float r = BitConverter.ToSingle(buffer); offset += 4; data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float g = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float g = BitConverter.ToSingle(buffer); offset += 4; data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float b = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float b = BitConverter.ToSingle(buffer); offset += 4; data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float a = BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float a = BitConverter.ToSingle(buffer); offset += 4; var colorVector = new Vector4(r, g, b, a); @@ -66,20 +66,16 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 4).CopyTo(buffer); - float r = BitConverter.ToSingle(buffer, 0); + float r = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - data.Slice(offset, 4).CopyTo(buffer); - float g = BitConverter.ToSingle(buffer, 0); + float g = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - data.Slice(offset, 4).CopyTo(buffer); - float b = BitConverter.ToSingle(buffer, 0); + float b = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - data.Slice(offset, 4).CopyTo(buffer); - float a = BitConverter.ToSingle(buffer, 0); + float a = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; var colorVector = new Vector4(r, g, b, a); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs index 78d557f30b..f3207b2f45 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -26,7 +26,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in { var color = default(TPixel); color.FromScaledVector4(Vector4.Zero); - byte[] buffer = new byte[4]; + Span buffer = stackalloc byte[4]; int offset = 0; for (int y = top; y < top + height; y++) @@ -37,8 +37,8 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int x = 0; x < pixelRow.Length; x++) { data.Slice(offset, 4).CopyTo(buffer); - Array.Reverse(buffer); - float intensity = 1.0f - BitConverter.ToSingle(buffer, 0); + buffer.Reverse(); + float intensity = 1.0f - BitConverter.ToSingle(buffer); offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); @@ -50,8 +50,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 4).CopyTo(buffer); - float intensity = 1.0f - BitConverter.ToSingle(buffer, 0); + float intensity = 1.0f - BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index ba8d2bd23e..d7243c6964 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -29,11 +29,6 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals /// private readonly MemoryAllocator memoryAllocator; - /// - /// A scratch buffer to reduce allocations. - /// - private readonly byte[] buffer = new byte[4]; - /// /// The global configuration. /// @@ -157,7 +152,9 @@ public void Encode(Image image, Stream stream, CancellationToken this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); using TiffStreamWriter writer = new(stream); - long ifdMarker = WriteHeader(writer); + Span buffer = stackalloc byte[4]; + + long ifdMarker = WriteHeader(writer, buffer); Image metadataImage = image; foreach (ImageFrame frame in image.Frames) @@ -171,7 +168,7 @@ public void Encode(Image image, Stream stream, CancellationToken long currentOffset = writer.BaseStream.Position; foreach ((long, uint) marker in this.frameMarkers) { - writer.WriteMarkerFast(marker.Item1, marker.Item2); + writer.WriteMarkerFast(marker.Item1, marker.Item2, buffer); } writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); @@ -181,14 +178,15 @@ public void Encode(Image image, Stream stream, CancellationToken /// Writes the TIFF file header. /// /// The to write data to. + /// Scratch buffer with minimum size of 2. /// /// The marker to write the first IFD offset. /// - public static long WriteHeader(TiffStreamWriter writer) + public static long WriteHeader(TiffStreamWriter writer, Span buffer) { - writer.Write(ByteOrderMarker); - writer.Write(TiffConstants.HeaderMagicNumber); - return writer.PlaceMarker(); + writer.Write(ByteOrderMarker, buffer); + writer.Write(TiffConstants.HeaderMagicNumber, buffer); + return writer.PlaceMarker(buffer); } /// @@ -307,20 +305,22 @@ private long WriteIfd(TiffStreamWriter writer, List entries) entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); - writer.Write((ushort)entries.Count); + Span buffer = stackalloc byte[4]; + + writer.Write((ushort)entries.Count, buffer); foreach (IExifValue entry in entries) { - writer.Write((ushort)entry.Tag); - writer.Write((ushort)entry.DataType); - writer.Write(ExifWriter.GetNumberOfComponents(entry)); + writer.Write((ushort)entry.Tag, buffer); + writer.Write((ushort)entry.DataType, buffer); + writer.Write(ExifWriter.GetNumberOfComponents(entry), buffer); uint length = ExifWriter.GetLength(entry); if (length <= 4) { - int sz = ExifWriter.WriteValue(entry, this.buffer, 0); + int sz = ExifWriter.WriteValue(entry, buffer, 0); DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); - writer.WritePadded(this.buffer.AsSpan(0, sz)); + writer.WritePadded(buffer.Slice(0, sz)); } else { @@ -328,12 +328,12 @@ private long WriteIfd(TiffStreamWriter writer, List entries) int sz = ExifWriter.WriteValue(entry, raw, 0); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); largeDataBlocks.Add(raw); - writer.Write(dataOffset); + writer.Write(dataOffset, buffer); dataOffset += (uint)(raw.Length + (raw.Length % 2)); } } - long nextIfdMarker = writer.PlaceMarker(); + long nextIfdMarker = writer.PlaceMarker(buffer); foreach (byte[] dataBlock in largeDataBlocks) { diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs index be32ca9ed6..3c2ad60846 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -10,13 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; /// internal sealed class TiffStreamWriter : IDisposable { - private static readonly byte[] PaddingBytes = new byte[4]; - - /// - /// A scratch buffer to reduce allocations. - /// - private readonly byte[] buffer = new byte[4]; - /// /// Initializes a new instance of the class. /// @@ -41,11 +34,12 @@ internal sealed class TiffStreamWriter : IDisposable /// /// Writes an empty four bytes to the stream, returning the offset to be written later. /// + /// Scratch buffer with minimum size of 4. /// The offset to be written later. - public long PlaceMarker() + public long PlaceMarker(Span buffer) { long offset = this.BaseStream.Position; - this.Write(0u); + this.Write(0u, buffer); return offset; } @@ -71,36 +65,38 @@ public long PlaceMarker() /// Writes a two-byte unsigned integer to the current stream. /// /// The two-byte unsigned integer to write. - public void Write(ushort value) + /// Scratch buffer with minimum size of 2. + public void Write(ushort value, Span buffer) { if (IsLittleEndian) { - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, value); + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); } else { - BinaryPrimitives.WriteUInt16BigEndian(this.buffer, value); + BinaryPrimitives.WriteUInt16BigEndian(buffer, value); } - this.BaseStream.Write(this.buffer.AsSpan(0, 2)); + this.BaseStream.Write(buffer.Slice(0, 2)); } /// /// Writes a four-byte unsigned integer to the current stream. /// /// The four-byte unsigned integer to write. - public void Write(uint value) + /// Scratch buffer with minimum size of 4. + public void Write(uint value, Span buffer) { if (IsLittleEndian) { - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); } else { - BinaryPrimitives.WriteUInt32BigEndian(this.buffer, value); + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); } - this.BaseStream.Write(this.buffer.AsSpan(0, 4)); + this.BaseStream.Write(buffer.Slice(0, 4)); } /// @@ -113,7 +109,10 @@ public void WritePadded(Span value) if (value.Length % 4 != 0) { - this.BaseStream.Write(PaddingBytes, 0, 4 - (value.Length % 4)); + // No allocation occurs, refers directly to assembly's data segment. + ReadOnlySpan paddingBytes = new byte[4] { 0x00, 0x00, 0x00, 0x00 }; + paddingBytes = paddingBytes[..(4 - (value.Length % 4))]; + this.BaseStream.Write(paddingBytes); } } @@ -122,18 +121,19 @@ public void WritePadded(Span value) /// /// The offset returned when placing the marker /// The four-byte unsigned integer to write. - public void WriteMarker(long offset, uint value) + /// Scratch buffer. + public void WriteMarker(long offset, uint value, Span buffer) { long back = this.BaseStream.Position; this.BaseStream.Seek(offset, SeekOrigin.Begin); - this.Write(value); + this.Write(value, buffer); this.BaseStream.Seek(back, SeekOrigin.Begin); } - public void WriteMarkerFast(long offset, uint value) + public void WriteMarkerFast(long offset, uint value, Span buffer) { this.BaseStream.Seek(offset, SeekOrigin.Begin); - this.Write(value); + this.Write(value, buffer); } /// diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs index 73ce216d8d..4646de7f82 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs @@ -211,8 +211,8 @@ private static byte[] WriteIfdTags64Bit(List values) foreach (IExifValue entry in values) { - writer.Write((ushort)entry.Tag); - writer.Write((ushort)entry.DataType); + writer.Write((ushort)entry.Tag, buffer); + writer.Write((ushort)entry.DataType, buffer); WriteLong8(writer, buffer, ExifWriter.GetNumberOfComponents(entry)); uint length = ExifWriter.GetLength(entry); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index 7907597854..8724147301 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -19,7 +19,7 @@ public void WriteHeader_WritesValidHeader() using (TiffStreamWriter writer = new(stream)) { - long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); + long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]); } Assert.Equal(new byte[] { 0x49, 0x49, 42, 0, 0x00, 0x00, 0x00, 0x00 }, stream.ToArray()); @@ -32,7 +32,7 @@ public void WriteHeader_ReturnsFirstIfdMarker() TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); using TiffStreamWriter writer = new(stream); - long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); + long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]); Assert.Equal(4, firstIfdMarker); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs index f6a1257f47..9b26ab2702 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs @@ -53,7 +53,7 @@ public void Write_WritesUInt16() { using var stream = new MemoryStream(); using var writer = new TiffStreamWriter(stream); - writer.Write(1234); + writer.Write(1234, stackalloc byte[2]); Assert.Equal(new byte[] { 0xD2, 0x04 }, stream.ToArray()); } @@ -63,7 +63,7 @@ public void Write_WritesUInt32() { using var stream = new MemoryStream(); using var writer = new TiffStreamWriter(stream); - writer.Write(12345678U); + writer.Write(12345678U, stackalloc byte[4]); Assert.Equal(new byte[] { 0x4E, 0x61, 0xBC, 0x00 }, stream.ToArray()); } @@ -89,16 +89,17 @@ public void WritePadded_WritesByteArray(byte[] bytes, byte[] expectedResult) public void WriteMarker_WritesToPlacedPosition() { using var stream = new MemoryStream(); + Span buffer = stackalloc byte[4]; using (var writer = new TiffStreamWriter(stream)) { - writer.Write(0x11111111); - long marker = writer.PlaceMarker(); - writer.Write(0x33333333); + writer.Write(0x11111111, buffer); + long marker = writer.PlaceMarker(buffer); + writer.Write(0x33333333, buffer); - writer.WriteMarker(marker, 0x12345678); + writer.WriteMarker(marker, 0x12345678, buffer); - writer.Write(0x44444444); + writer.Write(0x44444444, buffer); } Assert.Equal( From d9e8d79dddbd4619ab34757cd8ff035f58c1647a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:35:41 +0100 Subject: [PATCH 02/10] Reduced intermediate allocations: Webp --- .../Formats/Webp/BitReader/Vp8LBitReader.cs | 2 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 22 ++-- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 15 +-- .../Formats/Webp/Lossless/HistogramEncoder.cs | 32 ++++-- .../Formats/Webp/Lossless/HuffmanUtils.cs | 26 ++--- .../Formats/Webp/Lossless/PredictorEncoder.cs | 16 +-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 57 ++++------ .../Webp/Lossless/WebpLosslessDecoder.cs | 5 +- src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs | 2 +- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 20 ++-- .../Formats/Webp/Lossy/Vp8Histogram.cs | 25 ++--- .../Formats/Webp/Lossy/Vp8Residual.cs | 11 +- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 27 ++--- .../Formats/Webp/WebpAnimationDecoder.cs | 35 +++--- .../Formats/Webp/WebpChunkParsingUtils.cs | 26 +++-- .../Formats/Webp/WebpDecoderCore.cs | 104 ++++++++++-------- 17 files changed, 212 insertions(+), 221 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs index 8da717545f..659576cf11 100644 --- a/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs +++ b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs @@ -192,7 +192,7 @@ public void FillBitWindow() [MethodImpl(InliningOptions.ShortMethod)] private void ShiftBytes() { - System.Span dataSpan = this.Data!.Memory.Span; + Span dataSpan = this.Data!.Memory.Span; while (this.bitPos >= 8 && this.pos < this.len) { this.value >>= 8; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 02b1d0ab6a..8baf2cc156 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -23,7 +24,7 @@ internal abstract class BitWriterBase /// /// A scratch buffer to reduce allocations. /// - private readonly byte[] scratchBuffer = new byte[4]; + private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly /// /// Initializes a new instance of the class. @@ -90,8 +91,8 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) protected void WriteRiffHeader(Stream stream, uint riffSize) { stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, riffSize); - stream.Write(this.scratchBuffer.AsSpan(0, 4)); + BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); + stream.Write(this.scratchBuffer.Span.Slice(0, 4)); stream.Write(WebpConstants.WebpHeader); } @@ -128,7 +129,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.AsSpan(0, 4); + Span buf = this.scratchBuffer.Span.Slice(0, 4); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -151,7 +152,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.AsSpan(0, 4); + Span buf = this.scratchBuffer.Span.Slice(0, 4); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -182,7 +183,7 @@ protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) { uint size = (uint)iccProfileBytes.Length; - Span buf = this.scratchBuffer.AsSpan(0, 4); + Span buf = this.scratchBuffer.Span.Slice(0, 4); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -245,7 +246,7 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 32; } - Span buf = this.scratchBuffer.AsSpan(0, 4); + Span buf = this.scratchBuffer.Span.Slice(0, 4); stream.Write(WebpConstants.Vp8XMagicBytes); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); stream.Write(buf); @@ -256,4 +257,11 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); stream.Write(buf[..3]); } + + private unsafe struct ScratchBuffer + { + private fixed byte scratch[4]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 4); + } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 22bc195d64..9dc7912392 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -14,11 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; /// internal class Vp8LBitWriter : BitWriterBase { - /// - /// A scratch buffer to reduce allocations. - /// - private readonly byte[] scratchBuffer = new byte[8]; - /// /// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed. /// @@ -194,8 +189,9 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X stream.Write(WebpConstants.Vp8LMagicBytes); // Write Vp8 Header. - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, size); - stream.Write(this.scratchBuffer.AsSpan(0, 4)); + Span scratchBuffer = stackalloc byte[8]; + BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); + stream.Write(scratchBuffer.Slice(0, 4)); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); // Write the encoded bytes of the image to the stream. @@ -228,8 +224,9 @@ private void PutBitsFlushBits() this.BitWriterResize(extraSize); } - BinaryPrimitives.WriteUInt64LittleEndian(this.scratchBuffer, this.bits); - this.scratchBuffer.AsSpan(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); + Span scratchBuffer = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); + scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); this.cur += WriterBytes; this.bits >>= WriterBits; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 5ac3301519..dd59ed2097 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal class HistogramEncoder +internal static class HistogramEncoder { /// /// Number of partitions for the three dominant (literal, red and blue) symbol costs. @@ -27,7 +27,7 @@ internal class HistogramEncoder private const ushort InvalidHistogramSymbol = ushort.MaxValue; - public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, ushort[] histogramSymbols) + public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, Span histogramSymbols) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; @@ -148,7 +148,7 @@ private static void HistogramAnalyzeEntropyBin(List histograms, u } } - private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, ushort[] histogramSymbols) + private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, Span histogramSymbols) { var stats = new Vp8LStreaks(); var bitsEntropy = new Vp8LBitEntropy(); @@ -171,20 +171,28 @@ private static int HistogramCopyAndAnalyze(List origHistograms, L } } - int numUsed = histogramSymbols.Count(h => h != InvalidHistogramSymbol); + int numUsed = 0; + foreach (ushort h in histogramSymbols) + { + if (h != InvalidHistogramSymbol) + { + numUsed++; + } + } + return numUsed; } private static void HistogramCombineEntropyBin( List histograms, - ushort[] clusters, + Span clusters, ushort[] clusterMappings, Vp8LHistogram curCombo, ushort[] binMap, int numBins, double combineCostFactor) { - var binInfo = new HistogramBinInfo[BinSize]; + Span binInfo = stackalloc HistogramBinInfo[BinSize]; for (int idx = 0; idx < numBins; idx++) { binInfo[idx].First = -1; @@ -258,7 +266,7 @@ private static void HistogramCombineEntropyBin( /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values. /// - private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, ushort[] symbols) + private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span symbols) { bool doContinue = true; @@ -331,7 +339,7 @@ private static bool HistogramCombineStochastic(List histograms, i int maxSize = 9; // Fill the initial mapping. - int[] mappings = new int[histograms.Count]; + Span mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count]; for (int j = 0, iter = 0; iter < histograms.Count; iter++) { if (histograms[iter] == null) @@ -388,9 +396,9 @@ private static bool HistogramCombineStochastic(List histograms, i int bestIdx1 = histoPriorityList[0].Idx1; int bestIdx2 = histoPriorityList[0].Idx2; - int mappingIndex = Array.IndexOf(mappings, bestIdx2); - Span src = mappings.AsSpan(mappingIndex + 1, numUsed - mappingIndex - 1); - Span dst = mappings.AsSpan(mappingIndex); + int mappingIndex = mappings.IndexOf(bestIdx2); + Span src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1); + Span dst = mappings.Slice(mappingIndex); src.CopyTo(dst); // Merge the histograms and remove bestIdx2 from the list. @@ -528,7 +536,7 @@ private static void HistogramCombineGreedy(List histograms) } } - private static void HistogramRemap(List input, List output, ushort[] symbols) + private static void HistogramRemap(List input, List output, Span symbols) { int inSize = input.Count; int outSize = output.Count; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs index 18104331ce..39ad967e38 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs @@ -25,7 +25,7 @@ internal static class HuffmanUtils 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf }; - public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, HuffmanTree[] huffTree, HuffmanTreeCode huffCode) + public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) { int numSymbols = huffCode.NumSymbols; bufRle.AsSpan().Clear(); @@ -159,7 +159,7 @@ public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] c /// The size of the histogram. /// The tree depth limit. /// How many bits are used for the symbol. - public static void GenerateOptimalTree(HuffmanTree[] tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) + public static void GenerateOptimalTree(Span tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) { uint countMin; int treeSizeOrig = 0; @@ -177,7 +177,7 @@ public static void GenerateOptimalTree(HuffmanTree[] tree, uint[] histogram, int return; } - Span treePool = tree.AsSpan(treeSizeOrig); + Span treePool = tree.Slice(treeSizeOrig); // For block sizes with less than 64k symbols we never need to do a // second iteration of this loop. @@ -202,14 +202,8 @@ public static void GenerateOptimalTree(HuffmanTree[] tree, uint[] histogram, int } // Build the Huffman tree. -#if NET5_0_OR_GREATER - Span treeSlice = tree.AsSpan(0, treeSize); + Span treeSlice = tree.Slice(0, treeSize); treeSlice.Sort(HuffmanTree.Compare); -#else - HuffmanTree[] treeCopy = tree.AsSpan(0, treeSize).ToArray(); - Array.Sort(treeCopy, HuffmanTree.Compare); - treeCopy.AsSpan().CopyTo(tree); -#endif if (treeSize > 1) { @@ -312,12 +306,12 @@ public static int BuildHuffmanTable(Span table, int rootBits, int[] DebugGuard.MustBeGreaterThan(codeLengthsSize, 0, nameof(codeLengthsSize)); // sorted[codeLengthsSize] is a pre-allocated array for sorting symbols by code length. - int[] sorted = new int[codeLengthsSize]; + Span sorted = codeLengthsSize <= 64 ? stackalloc int[codeLengthsSize] : new int[codeLengthsSize]; int totalSize = 1 << rootBits; // total size root table + 2nd level table. int len; // current code length. int symbol; // symbol index in original or sorted table. - int[] counts = new int[WebpConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. - int[] offsets = new int[WebpConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length. + Span counts = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. + Span offsets = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length. // Build histogram of code lengths. for (symbol = 0; symbol < codeLengthsSize; ++symbol) @@ -544,8 +538,8 @@ private static int CodeRepeatedValues(int repetitions, Span to private static void ConvertBitDepthsToSymbols(HuffmanTreeCode tree) { // 0 bit-depth means that the symbol does not exist. - uint[] nextCode = new uint[WebpConstants.MaxAllowedCodeLength + 1]; - int[] depthCount = new int[WebpConstants.MaxAllowedCodeLength + 1]; + Span nextCode = stackalloc uint[WebpConstants.MaxAllowedCodeLength + 1]; + Span depthCount = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; int len = tree.NumSymbols; for (int i = 0; i < len; i++) @@ -603,7 +597,7 @@ private static uint ReverseBits(int numBits, uint bits) /// Returns the table width of the next 2nd level table. count is the histogram of bit lengths for the remaining symbols, /// len is the code length of the next processed symbol. /// - private static int NextTableBitSize(int[] count, int len, int rootBits) + private static int NextTableBitSize(ReadOnlySpan count, int len, int rootBits) { int left = 1 << (len - rootBits); while (len < WebpConstants.MaxAllowedCodeLength) diff --git a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs index 689c63f5b1..2170eb1985 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs @@ -57,11 +57,13 @@ public static void ResidualImage( Span scratch = stackalloc short[8]; // TODO: Can we optimize this? - int[][] histo = new int[4][]; - for (int i = 0; i < 4; i++) + int[][] histo = { - histo[i] = new int[256]; - } + new int[256], + new int[256], + new int[256], + new int[256] + }; if (lowEffort) { @@ -233,7 +235,7 @@ private static int GetBestPredictorForTile( Span maxDiffs = MemoryMarshal.Cast(currentRow[(width + 1)..]); float bestDiff = MaxDiffCost; int bestMode = 0; - uint[] residuals = new uint[1 << WebpConstants.MaxTransformBits]; + Span residuals = stackalloc uint[1 << WebpConstants.MaxTransformBits]; // 256 bytes for (int i = 0; i < 4; i++) { histoArgb[i].AsSpan().Clear(); @@ -299,9 +301,7 @@ private static int GetBestPredictorForTile( if (curDiff < bestDiff) { - int[][] tmp = histoArgb; - histoArgb = bestHisto; - bestHisto = tmp; + (bestHisto, histoArgb) = (histoArgb, bestHisto); bestDiff = curDiff; bestMode = mode; } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d678da6028..e3c2797bf3 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -23,7 +23,7 @@ internal class Vp8LEncoder : IDisposable /// /// Scratch buffer to reduce allocations. /// - private readonly int[] scratch = new int[256]; + private ScratchBuffer scratch; // mutable struct, don't make readonly private readonly int[][] histoArgb = { new int[256], new int[256], new int[256], new int[256] }; @@ -549,12 +549,8 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf // bgra data with transformations applied. Span bgra = this.EncodedData.GetSpan(); int histogramImageXySize = LosslessUtils.SubSampleSize(width, this.HistoBits) * LosslessUtils.SubSampleSize(height, this.HistoBits); - ushort[] histogramSymbols = new ushort[histogramImageXySize]; - HuffmanTree[] huffTree = new HuffmanTree[3 * WebpConstants.CodeLengthCodes]; - for (int i = 0; i < huffTree.Length; i++) - { - huffTree[i] = default; - } + Span histogramSymbols = histogramImageXySize <= 64 ? stackalloc ushort[histogramImageXySize] : new ushort[histogramImageXySize]; + Span huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes]; if (useCache) { @@ -607,10 +603,6 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf int histogramImageSize = histogramImage.Count; int bitArraySize = 5 * histogramImageSize; HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[bitArraySize]; - for (int i = 0; i < huffmanCodes.Length; i++) - { - huffmanCodes[i] = default; - } GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); @@ -702,7 +694,7 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf /// private void EncodePalette(bool lowEffort) { - Span tmpPalette = new uint[WebpConstants.MaxPaletteSize]; + Span tmpPalette = stackalloc uint[WebpConstants.MaxPaletteSize]; int paletteSize = this.PaletteSize; Span palette = this.Palette.Memory.Span; this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); @@ -763,7 +755,7 @@ private void ApplyCrossColorFilter(int width, int height, bool lowEffort) int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits); - PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch); + PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch.Span); this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); @@ -778,16 +770,7 @@ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8L ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol. HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[5]; - for (int i = 0; i < huffmanCodes.Length; i++) - { - huffmanCodes[i] = default; - } - - HuffmanTree[] huffTree = new HuffmanTree[3UL * WebpConstants.CodeLengthCodes]; - for (int i = 0; i < huffTree.Length; i++) - { - huffTree[i] = default; - } + Span huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes]; // Calculate backward references from the image pixels. hashChain.Fill(bgra, quality, width, height, lowEffort); @@ -847,10 +830,10 @@ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8L this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes); } - private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) + private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) { int count = 0; - Span symbols = this.scratch.AsSpan(0, 2); + Span symbols = this.scratch.Span.Slice(0, 2); symbols.Clear(); const int maxBits = 8; const int maxSymbol = 1 << maxBits; @@ -901,7 +884,7 @@ private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, } } - private void StoreFullHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) + private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) { int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; @@ -1013,7 +996,7 @@ private void StoreHuffmanTreeOfHuffmanTreeToBitMask(byte[] codeLengthBitDepth) } } - private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, ushort[] histogramSymbols, HuffmanTreeCode[] huffmanCodes) + private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span histogramSymbols, HuffmanTreeCode[] huffmanCodes) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); @@ -1143,8 +1126,8 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, prevRow = currentRow; } - double[] entropyComp = new double[(int)HistoIx.HistoTotal]; - double[] entropy = new double[(int)EntropyIx.NumEntropyIx]; + Span entropyComp = stackalloc double[(int)HistoIx.HistoTotal]; + Span entropy = stackalloc double[(int)EntropyIx.NumEntropyIx]; int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen; // Let's add one zero to the predicted histograms. The zeros are removed @@ -1647,11 +1630,7 @@ private static void GetHuffBitLengthsAndCodes(List histogramImage // Create Huffman trees. bool[] bufRle = new bool[maxNumSymbols]; - HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols]; - for (int i = 0; i < huffTree.Length; i++) - { - huffTree[i] = default; - } + Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols]; for (int i = 0; i < histogramImage.Count; i++) { @@ -1849,4 +1828,14 @@ public void Dispose() this.TransformData.Dispose(); this.HashChain.Dispose(); } + + /// + /// Scratch buffer to reduce allocations. + /// + private unsafe struct ScratchBuffer + { + private fixed int scratch[256]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 256); + } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 84ddd4b785..19ea424199 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -498,10 +498,7 @@ private void ReadHuffmanCodes(Vp8LDecoder decoder, int xSize, int ySize, int col private int ReadHuffmanCode(int alphabetSize, int[] codeLengths, Span table) { bool simpleCode = this.bitReader.ReadBit(); - for (int i = 0; i < alphabetSize; i++) - { - codeLengths[i] = 0; - } + codeLengths.AsSpan(0, alphabetSize).Clear(); if (simpleCode) { diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs index 76de2e8f4a..e9eb1110b0 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -121,7 +121,7 @@ public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Seg var rdi4 = new Vp8ModeScore(); var rdTmp = new Vp8ModeScore(); var res = new Vp8Residual(); - Span tmpLevels = new short[16]; + Span tmpLevels = stackalloc short[16]; do { const int numBlocks = 1; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index b33ef57a6b..7211f93766 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -374,7 +374,7 @@ public int FastMbAnalyze(uint quality) } else { - byte[] modes = new byte[16]; // DC4 + Span modes = stackalloc byte[16]; // DC4 this.SetIntra4Mode(modes); } @@ -407,7 +407,7 @@ public int MbAnalyzeBestIntra16Mode() public int MbAnalyzeBestIntra4Mode(int bestAlpha) { - byte[] modes = new byte[16]; + Span modes = stackalloc byte[16]; const int maxMode = MaxIntra4Mode; Vp8Histogram totalHisto = new(); int curHisto = 0; @@ -494,13 +494,13 @@ public void SetIntra16Mode(int mode) this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16; } - public void SetIntra4Mode(byte[] modes) + public void SetIntra4Mode(ReadOnlySpan modes) { int modesIdx = 0; int predIdx = this.PredIdx; for (int y = 4; y > 0; y--) { - modes.AsSpan(modesIdx, 4).CopyTo(this.Preds.AsSpan(predIdx)); + modes.Slice(modesIdx, 4).CopyTo(this.Preds.AsSpan(predIdx)); predIdx += this.predsWidth; modesIdx += 4; } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index eefc4cd0a0..f17d965e87 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -329,7 +329,7 @@ public void Encode(Image image, Stream stream) int uvStride = (yStride + 1) >> 1; Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); - int[] alphas = new int[WebpConstants.MaxAlpha + 1]; + Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; this.alpha /= totalMb; @@ -625,15 +625,15 @@ private void ResetBoundaryPredictions() } // Simplified k-Means, to assign Nb segments based on alpha-histogram. - private void AssignSegments(int[] alphas) + private void AssignSegments(ReadOnlySpan alphas) { int nb = this.SegmentHeader.NumSegments < NumMbSegments ? this.SegmentHeader.NumSegments : NumMbSegments; - int[] centers = new int[NumMbSegments]; + Span centers = stackalloc int[NumMbSegments]; int weightedAverage = 0; - int[] map = new int[WebpConstants.MaxAlpha + 1]; + Span map = stackalloc int[WebpConstants.MaxAlpha + 1]; int n, k; - int[] accum = new int[NumMbSegments]; - int[] distAccum = new int[NumMbSegments]; + Span accum = stackalloc int[NumMbSegments]; + Span distAccum = stackalloc int[NumMbSegments]; // Bracket the input. for (n = 0; n <= WebpConstants.MaxAlpha && alphas[n] == 0; ++n) @@ -719,7 +719,7 @@ private void AssignSegments(int[] alphas) this.SetSegmentAlphas(centers, weightedAverage); } - private void SetSegmentAlphas(int[] centers, int mid) + private void SetSegmentAlphas(ReadOnlySpan centers, int mid) { int nb = this.SegmentHeader.NumSegments; Vp8SegmentInfo[] dqm = this.SegmentInfos; @@ -840,7 +840,7 @@ private void SetupFilterStrength() private void SetSegmentProbas() { - int[] p = new int[NumMbSegments]; + Span p = stackalloc int[NumMbSegments]; int n; for (n = 0; n < this.Mbw * this.Mbh; ++n) @@ -931,7 +931,7 @@ private unsafe void SetupMatrices(Vp8SegmentInfo[] dqm) } } - private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span y, Span u, Span v, int yStride, int uvStride, int[] alphas, out int uvAlpha) + private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span y, Span u, Span v, int yStride, int uvStride, Span alphas, out int uvAlpha) { int alpha = 0; uvAlpha = 0; @@ -952,7 +952,7 @@ private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span alphas, out int bestUvAlpha) { it.SetIntra16Mode(0); // default: Intra16, DC_PRED it.SetSkip(false); // not skipped. diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs index 4036fb2844..2ace43d2d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs @@ -10,12 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy; internal sealed class Vp8Histogram { - private readonly int[] scratch = new int[16]; - - private readonly short[] output = new short[16]; - - private readonly int[] distribution = new int[MaxCoeffThresh + 1]; - /// /// Size of histogram used by CollectHistogram. /// @@ -47,17 +41,20 @@ public int GetAlpha() public void CollectHistogram(Span reference, Span pred, int startBlock, int endBlock) { + Span scratch = stackalloc int[16]; + Span output = stackalloc short[16]; + Span distribution = stackalloc int[MaxCoeffThresh + 1]; + int j; - this.distribution.AsSpan().Clear(); for (j = startBlock; j < endBlock; j++) { - Vp8Encoding.FTransform(reference[WebpLookupTables.Vp8DspScan[j]..], pred[WebpLookupTables.Vp8DspScan[j]..], this.output, this.scratch); + Vp8Encoding.FTransform(reference[WebpLookupTables.Vp8DspScan[j]..], pred[WebpLookupTables.Vp8DspScan[j]..], output, scratch); // Convert coefficients to bin. if (Avx2.IsSupported) { // Load. - ref short outputRef = ref MemoryMarshal.GetReference(this.output); + ref short outputRef = ref MemoryMarshal.GetReference(output); Vector256 out0 = Unsafe.As>(ref outputRef); // v = abs(out) >> 3 @@ -73,21 +70,21 @@ public void CollectHistogram(Span reference, Span pred, int startBlo // Convert coefficients to bin. for (int k = 0; k < 16; ++k) { - ++this.distribution[this.output[k]]; + ++distribution[output[k]]; } } else { for (int k = 0; k < 16; ++k) { - int v = Math.Abs(this.output[k]) >> 3; + int v = Math.Abs(output[k]) >> 3; int clippedValue = ClipMax(v, MaxCoeffThresh); - ++this.distribution[clippedValue]; + ++distribution[clippedValue]; } } } - this.SetHistogramData(this.distribution); + this.SetHistogramData(distribution); } public void Merge(Vp8Histogram other) @@ -103,7 +100,7 @@ public void Merge(Vp8Histogram other) } } - private void SetHistogramData(int[] distribution) + private void SetHistogramData(ReadOnlySpan distribution) { int maxValue = 0; int lastNonZero = 1; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs index 7384379dab..1770415062 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs @@ -15,10 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy; /// internal class Vp8Residual { - private readonly byte[] scratch = new byte[32]; - - private readonly ushort[] scratchUShort = new ushort[16]; - public int First { get; set; } public int Last { get; set; } @@ -162,9 +158,10 @@ public int GetResidualCost(int ctx0) if (Avx2.IsSupported) { - Span ctxs = this.scratch.AsSpan(0, 16); - Span levels = this.scratch.AsSpan(16, 16); - Span absLevels = this.scratchUShort.AsSpan(); + Span scratch = stackalloc byte[32]; + Span ctxs = scratch.Slice(0, 16); + Span levels = scratch.Slice(16); + Span absLevels = stackalloc ushort[16]; // Precompute clamped levels and contexts, packed to 8b. ref short outputRef = ref MemoryMarshal.GetReference(this.Coeffs); diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 96ed8903a0..cb13825bc2 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -34,16 +34,6 @@ internal sealed class WebpLossyDecoder /// private readonly Configuration configuration; - /// - /// Scratch buffer to reduce allocations. - /// - private readonly int[] scratch = new int[16]; - - /// - /// Another scratch buffer to reduce allocations. - /// - private readonly byte[] scratchBytes = new byte[4]; - /// /// Initializes a new instance of the class. /// @@ -409,6 +399,9 @@ private void ReconstructRow(Vp8Decoder dec) topYuv.V.CopyTo(yuv[(vOff - WebpConstants.Bps)..]); } + Span scratch = stackalloc int[16]; + Span scratchBytes = stackalloc byte[4]; + // Predict and add residuals. if (block.IsI4x4) { @@ -448,7 +441,7 @@ private void ReconstructRow(Vp8Decoder dec) LossyUtils.TM4(dst, yuv, offset); break; case 2: - LossyUtils.VE4(dst, yuv, offset, this.scratchBytes); + LossyUtils.VE4(dst, yuv, offset, scratchBytes); break; case 3: LossyUtils.HE4(dst, yuv, offset); @@ -473,7 +466,7 @@ private void ReconstructRow(Vp8Decoder dec) break; } - DoTransform(bits, coeffs.AsSpan(n * 16), dst, this.scratch); + DoTransform(bits, coeffs.AsSpan(n * 16), dst, scratch); } } else @@ -508,7 +501,7 @@ private void ReconstructRow(Vp8Decoder dec) { for (int n = 0; n < 16; ++n, bits <<= 2) { - DoTransform(bits, coeffs.AsSpan(n * 16), yDst[WebpConstants.Scan[n]..], this.scratch); + DoTransform(bits, coeffs.AsSpan(n * 16), yDst[WebpConstants.Scan[n]..], scratch); } } } @@ -547,8 +540,8 @@ private void ReconstructRow(Vp8Decoder dec) break; } - DoUVTransform(bitsUv, coeffs.AsSpan(16 * 16), uDst, this.scratch); - DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst, this.scratch); + DoUVTransform(bitsUv, coeffs.AsSpan(16 * 16), uDst, scratch); + DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst, scratch); // Stash away top samples for next block. if (mby < dec.MbHeight - 1) @@ -875,14 +868,14 @@ private bool ParseResiduals(Vp8Decoder dec, Vp8BitReader br, Vp8MacroBlock mb) else { // Parse DC - short[] dc = new short[16]; + Span dc = stackalloc short[16]; int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs); int nz = GetCoeffs(br, bands[1], ctx, q.Y2Mat, 0, dc); mb.NoneZeroDcCoeffs = leftMb.NoneZeroDcCoeffs = (uint)(nz > 0 ? 1 : 0); if (nz > 1) { // More than just the DC -> perform the full transform. - LossyUtils.TransformWht(dc, dst, this.scratch); + LossyUtils.TransformWht(dc, dst, stackalloc int[16]); } else { diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index abaa85ef18..21337ce6c8 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -17,11 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// internal class WebpAnimationDecoder : IDisposable { - /// - /// Reusable buffer. - /// - private readonly byte[] buffer = new byte[4]; - /// /// Used for allocating memory during the decoding operations. /// @@ -89,11 +84,12 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount; + Span buffer = stackalloc byte[4]; uint frameCount = 0; int remainingBytes = (int)completeDataSize; while (remainingBytes > 0) { - WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); remainingBytes -= 4; switch (chunkType) { @@ -103,7 +99,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat break; case WebpChunkType.Xmp: case WebpChunkType.Exif: - WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, this.buffer); + WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer); break; default: WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data"); @@ -134,15 +130,16 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima { AnimationFrameData frameData = this.ReadFrameHeader(stream); long streamStartPosition = stream.Position; + Span buffer = stackalloc byte[4]; - WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); bool hasAlpha = false; byte alphaChunkHeader = 0; if (chunkType is WebpChunkType.Alpha) { alphaChunkHeader = this.ReadAlphaData(stream); hasAlpha = true; - chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); } WebpImageInfo? webpInfo = null; @@ -150,12 +147,12 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima switch (chunkType) { case WebpChunkType.Vp8: - webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); + webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); features.Alpha = hasAlpha; features.AlphaChunkHeader = alphaChunkHeader; break; case WebpChunkType.Vp8L: - webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); break; default: WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L"); @@ -226,7 +223,7 @@ private byte ReadAlphaData(BufferedReadStream stream) { this.alphaData?.Dispose(); - uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, stackalloc byte[4]); int alphaDataSize = (int)(alphaChunkSize - 1); this.alphaData = this.memoryAllocator.Allocate(alphaDataSize); @@ -353,24 +350,26 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba /// Animation frame data. private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) { + Span buffer = stackalloc byte[4]; + AnimationFrameData data = new() { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer), + DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer), + X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer), + Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1, + Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1, + Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, // Frame duration. - Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) }; byte flags = (byte)stream.ReadByte(); diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index e4acdf311c..a7ae474e46 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -18,7 +18,7 @@ internal static class WebpChunkParsingUtils /// Reads the header of a lossy webp image. /// /// Information about this webp image. - public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) { // VP8 data size (not including this 4 bytes). int bytesRead = stream.Read(buffer, 0, 4); @@ -77,7 +77,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); } - if (!buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) + if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } @@ -91,7 +91,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); - tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2)); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)); uint height = tmp & 0x3fff; sbyte yScale = (sbyte)(tmp >> 6); remaining -= 7; @@ -140,7 +140,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe /// Reads the header of a lossless webp image. /// /// Information about this image. - public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) { // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); @@ -195,7 +195,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. /// /// Information about this webp image. - public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span buffer, WebpFeatures features) { uint fileSize = ReadChunkSize(stream, buffer); @@ -253,7 +253,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buf /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer) + public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -271,9 +271,11 @@ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer /// The stream to read the data from. /// Buffer to store the data read from the stream. /// The chunk size in bytes. - public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer) + public static uint ReadChunkSize(BufferedReadStream stream, Span buffer) { - if (stream.Read(buffer, 0, 4) == 4) + DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length"); + + if (stream.Read(buffer) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; @@ -290,9 +292,11 @@ public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer) /// /// Thrown if the input stream is not valid. /// - public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buffer) + public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span buffer) { - if (stream.Read(buffer, 0, 4) == 4) + DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length"); + + if (stream.Read(buffer) == 4) { var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); return chunkType; @@ -306,7 +310,7 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buff /// If there are more such chunks, readers MAY ignore all except the first one. /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. /// - public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, byte[] buffer) + public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, Span buffer) { long streamLength = stream.Length; while (stream.Position < streamLength) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index c158df44d9..0d19dda023 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -18,11 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { - /// - /// Reusable buffer. - /// - private readonly byte[] buffer = new byte[4]; - /// /// General configuration options. /// @@ -80,8 +75,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken try { ImageMetadata metadata = new(); + Span buffer = stackalloc byte[4]; - uint fileSize = this.ReadImageHeader(stream); + uint fileSize = ReadImageHeader(stream, buffer); using (this.webImageInfo = this.ReadVp8Info(stream, metadata)) { @@ -112,7 +108,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken // There can be optional chunks after the image data, like EXIF and XMP. if (this.webImageInfo.Features != null) { - this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features); + this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features, buffer); } return image; @@ -128,7 +124,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken /// public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ReadImageHeader(stream); + ReadImageHeader(stream, stackalloc byte[4]); ImageMetadata metadata = new(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) @@ -144,8 +140,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat /// Reads and skips over the image header. /// /// The stream to decode from. + /// Temporary buffer. /// The file size in bytes. - private uint ReadImageHeader(BufferedReadStream stream) + private static uint ReadImageHeader(BufferedReadStream stream, Span buffer) { // Skip FourCC header, we already know its a RIFF file at this point. stream.Skip(4); @@ -153,7 +150,7 @@ private uint ReadImageHeader(BufferedReadStream stream) // Read file size. // The size of the file in bytes starting at offset 8. // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. - uint fileSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); + uint fileSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); // Skip 'WEBP' from the header. stream.Skip(4); @@ -172,35 +169,36 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad { WebpMetadata webpMetadata = metadata.GetFormatMetadata(WebpFormat.Instance); - WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + Span buffer = stackalloc byte[4]; + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: webpMetadata.FileFormat = WebpFileFormatType.Lossy; - return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); + return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); case WebpChunkType.Vp8L: webpMetadata.FileFormat = WebpFileFormatType.Lossless; - return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); + return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); case WebpChunkType.Vp8X: - WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, this.buffer, features); + WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); while (stream.Position < stream.Length) { - chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Vp8) { webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); + webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); } else if (chunkType == WebpChunkType.Vp8L) { webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); + webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { - bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha); + bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { return webpInfos; @@ -209,7 +207,7 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad else { // Ignore unknown chunks. - uint chunkSize = this.ReadChunkSize(stream); + uint chunkSize = ReadChunkSize(stream, buffer); stream.Skip((int)chunkSize); } } @@ -230,34 +228,36 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad /// The chunk type. /// The webp image features. /// For identify, the alpha data should not be read. + /// Temporary buffer. /// true, if its a alpha chunk. private bool ParseOptionalExtendedChunks( BufferedReadStream stream, ImageMetadata metadata, WebpChunkType chunkType, WebpFeatures features, - bool ignoreAlpha) + bool ignoreAlpha, + Span buffer) { switch (chunkType) { case WebpChunkType.Iccp: - this.ReadIccProfile(stream, metadata); + this.ReadIccProfile(stream, metadata, buffer); break; case WebpChunkType.Exif: - this.ReadExifProfile(stream, metadata); + this.ReadExifProfile(stream, metadata, buffer); break; case WebpChunkType.Xmp: - this.ReadXmpProfile(stream, metadata); + this.ReadXmpProfile(stream, metadata, buffer); break; case WebpChunkType.AnimationParameter: - this.ReadAnimationParameters(stream, features); + ReadAnimationParameters(stream, features, buffer); return true; case WebpChunkType.Alpha: - this.ReadAlphaData(stream, features, ignoreAlpha); + this.ReadAlphaData(stream, features, ignoreAlpha, buffer); break; default: WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); @@ -273,7 +273,8 @@ private bool ParseOptionalExtendedChunks( /// The stream to decode from. /// The image metadata. /// The webp features. - private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features) + /// Temporary buffer. + private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span buffer) { if (this.skipMetadata || (!features.ExifProfile && !features.XmpMetaData)) { @@ -284,19 +285,19 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada while (stream.Position < streamLength) { // Read chunk header. - WebpChunkType chunkType = this.ReadChunkType(stream); + WebpChunkType chunkType = ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) { - this.ReadExifProfile(stream, metadata); + this.ReadExifProfile(stream, metadata, buffer); } else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) { - this.ReadXmpProfile(stream, metadata); + this.ReadXmpProfile(stream, metadata, buffer); } else { // Skip duplicate XMP or EXIF chunk. - uint chunkLength = this.ReadChunkSize(stream); + uint chunkLength = ReadChunkSize(stream, buffer); stream.Skip((int)chunkLength); } } @@ -307,9 +308,10 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada /// /// The stream to decode from. /// The image metadata. - private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata) + /// Temporary buffer. + private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, Span buffer) { - uint exifChunkSize = this.ReadChunkSize(stream); + uint exifChunkSize = ReadChunkSize(stream, buffer); if (this.skipMetadata) { stream.Skip((int)exifChunkSize); @@ -333,9 +335,10 @@ private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata) /// /// The stream to decode from. /// The image metadata. - private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata) + /// Temporary buffer. + private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, Span buffer) { - uint xmpChunkSize = this.ReadChunkSize(stream); + uint xmpChunkSize = ReadChunkSize(stream, buffer); if (this.skipMetadata) { stream.Skip((int)xmpChunkSize); @@ -359,9 +362,10 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata) /// /// The stream to decode from. /// The image metadata. - private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata) + /// Temporary buffer. + private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, Span buffer) { - uint iccpChunkSize = this.ReadChunkSize(stream); + uint iccpChunkSize = ReadChunkSize(stream, buffer); if (this.skipMetadata) { stream.Skip((int)iccpChunkSize); @@ -388,22 +392,23 @@ private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata) /// /// The stream to decode from. /// The webp features. - private void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features) + /// Temporary buffer. + private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features, Span buffer) { features.Animation = true; - uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); + uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); byte blue = (byte)stream.ReadByte(); byte green = (byte)stream.ReadByte(); byte red = (byte)stream.ReadByte(); byte alpha = (byte)stream.ReadByte(); features.AnimationBackgroundColor = new Color(new Rgba32(red, green, blue, alpha)); - int bytesRead = stream.Read(this.buffer, 0, 2); + int bytesRead = stream.Read(buffer, 0, 2); if (bytesRead != 2) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count"); } - features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(this.buffer); + features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer); } /// @@ -412,9 +417,10 @@ private void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures fea /// The stream to decode from. /// The features. /// if set to true, skips the chunk data. - private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha) + /// Temporary buffer. + private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha, Span buffer) { - uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); if (ignoreAlpha) { stream.Skip((int)alphaChunkSize); @@ -436,14 +442,15 @@ private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, boo /// Identifies the chunk type from the chunk. /// /// The stream to decode from. + /// Temporary buffer. /// /// Thrown if the input stream is not valid. /// - private WebpChunkType ReadChunkType(BufferedReadStream stream) + private static WebpChunkType ReadChunkType(BufferedReadStream stream, Span buffer) { - if (stream.Read(this.buffer, 0, 4) == 4) + if (stream.Read(buffer, 0, 4) == 4) { - return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); } throw new ImageFormatException("Invalid Webp data."); @@ -454,13 +461,14 @@ private WebpChunkType ReadChunkType(BufferedReadStream stream) /// so the chunk size will be increased by 1 in those cases. /// /// The stream to decode from. + /// Temporary buffer. /// The chunk size in bytes. /// Invalid data. - private uint ReadChunkSize(BufferedReadStream stream) + private static uint ReadChunkSize(BufferedReadStream stream, Span buffer) { - if (stream.Read(this.buffer, 0, 4) == 4) + if (stream.Read(buffer, 0, 4) == 4) { - uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; } From 31424c0b6477d9451a64ce22578a1732ef64c20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:36:00 +0100 Subject: [PATCH 03/10] Reduced intermediate allocations: Png --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 57 +++++++++++--------- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 50 +++++++++-------- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3ecc363fa4..d1d29dca6b 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -28,11 +28,6 @@ namespace SixLabors.ImageSharp.Formats.Png; /// internal sealed class PngDecoderCore : IImageDecoderInternals { - /// - /// Reusable buffer. - /// - private readonly byte[] buffer = new byte[4]; - /// /// The general decoder options. /// @@ -154,9 +149,11 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.currentStream = stream; this.currentStream.Skip(8); Image image = null; + Span buffer = stackalloc byte[20]; + try { - while (this.TryReadChunk(out PngChunk chunk)) + while (this.TryReadChunk(buffer, out PngChunk chunk)) { try { @@ -252,10 +249,13 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; + Span buffer = stackalloc byte[20]; + this.currentStream.Skip(8); + try { - while (this.TryReadChunk(out PngChunk chunk)) + while (this.TryReadChunk(buffer, out PngChunk chunk)) { try { @@ -1401,9 +1401,11 @@ private int ReadNextDataChunk() return 0; } - this.currentStream.Read(this.buffer, 0, 4); + Span buffer = stackalloc byte[20]; + + this.currentStream.Read(buffer, 0, 4); - if (this.TryReadChunk(out PngChunk chunk)) + if (this.TryReadChunk(buffer, out PngChunk chunk)) { if (chunk.Type == PngChunkType.Data) { @@ -1420,11 +1422,12 @@ private int ReadNextDataChunk() /// /// Reads a chunk from the stream. /// + /// Temporary buffer. /// The image format chunk. /// /// The . /// - private bool TryReadChunk(out PngChunk chunk) + private bool TryReadChunk(Span buffer, out PngChunk chunk) { if (this.nextChunk != null) { @@ -1435,7 +1438,7 @@ private bool TryReadChunk(out PngChunk chunk) return true; } - if (!this.TryReadChunkLength(out int length)) + if (!this.TryReadChunkLength(buffer, out int length)) { chunk = default; @@ -1446,7 +1449,7 @@ private bool TryReadChunk(out PngChunk chunk) while (length < 0 || length > (this.currentStream.Length - this.currentStream.Position)) { // Not a valid chunk so try again until we reach a known chunk. - if (!this.TryReadChunkLength(out length)) + if (!this.TryReadChunkLength(buffer, out length)) { chunk = default; @@ -1454,7 +1457,7 @@ private bool TryReadChunk(out PngChunk chunk) } } - PngChunkType type = this.ReadChunkType(); + PngChunkType type = this.ReadChunkType(buffer); // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. @@ -1471,7 +1474,7 @@ private bool TryReadChunk(out PngChunk chunk) type: type, data: this.ReadChunkData(length)); - this.ValidateChunk(chunk); + this.ValidateChunk(chunk, buffer); // Restore the stream position for IDAT chunks, because it will be decoded later and // was only read to verifying the CRC is correct. @@ -1487,9 +1490,10 @@ private bool TryReadChunk(out PngChunk chunk) /// Validates the png chunk. /// /// The . - private void ValidateChunk(in PngChunk chunk) + /// Temporary buffer. + private void ValidateChunk(in PngChunk chunk, Span buffer) { - uint inputCrc = this.ReadChunkCrc(); + uint inputCrc = this.ReadChunkCrc(buffer); if (chunk.IsCritical) { @@ -1513,13 +1517,14 @@ private void ValidateChunk(in PngChunk chunk) /// /// Reads the cycle redundancy chunk from the data. /// + /// Temporary buffer. [MethodImpl(InliningOptions.ShortMethod)] - private uint ReadChunkCrc() + private uint ReadChunkCrc(Span buffer) { uint crc = 0; - if (this.currentStream.Read(this.buffer, 0, 4) == 4) + if (this.currentStream.Read(buffer, 0, 4) == 4) { - crc = BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + crc = BinaryPrimitives.ReadUInt32BigEndian(buffer); } return crc; @@ -1554,15 +1559,16 @@ private IMemoryOwner ReadChunkData(int length) /// /// Identifies the chunk type from the chunk. /// + /// Temporary buffer. /// /// Thrown if the input stream is not valid. /// [MethodImpl(InliningOptions.ShortMethod)] - private PngChunkType ReadChunkType() + private PngChunkType ReadChunkType(Span buffer) { - if (this.currentStream.Read(this.buffer, 0, 4) == 4) + if (this.currentStream.Read(buffer, 0, 4) == 4) { - return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); } PngThrowHelper.ThrowInvalidChunkType(); @@ -1574,16 +1580,17 @@ private PngChunkType ReadChunkType() /// /// Attempts to read the length of the next chunk. /// + /// Temporary buffer. /// The result length. If the return type is this parameter is passed uninitialized. /// /// Whether the length was read. /// [MethodImpl(InliningOptions.ShortMethod)] - private bool TryReadChunkLength(out int result) + private bool TryReadChunkLength(Span buffer, out int result) { - if (this.currentStream.Read(this.buffer, 0, 4) == 4) + if (this.currentStream.Read(buffer, 0, 4) == 4) { - result = BinaryPrimitives.ReadInt32BigEndian(this.buffer); + result = BinaryPrimitives.ReadInt32BigEndian(buffer); return true; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5794da3d56..a2edbc4c3f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -38,15 +38,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// private readonly Configuration configuration; - /// - /// Reusable buffer for writing general data. - /// - private readonly byte[] buffer = new byte[8]; - /// /// Reusable buffer for writing chunk data. /// - private readonly byte[] chunkDataBuffer = new byte[16]; + private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly /// /// The encoder with options @@ -576,9 +571,9 @@ private void WriteHeaderChunk(Stream stream) filterMethod: 0, interlaceMethod: this.interlaceMode); - header.WriteTo(this.chunkDataBuffer); + header.WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size); + this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); } /// @@ -652,9 +647,9 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta) return; } - PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer); + PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); - this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size); + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size); } /// @@ -880,9 +875,9 @@ private void WriteGammaChunk(Stream stream) // 4-byte unsigned integer of gamma * 100,000. uint gammaValue = (uint)(this.gamma * 100_000F); - BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), gammaValue); + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue); - this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer, 0, 4); + this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); } } @@ -899,7 +894,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) return; } - Span alpha = this.chunkDataBuffer.AsSpan(); + Span alpha = this.chunkDataBuffer.Span; if (pngMetadata.ColorType == PngColorType.Rgb) { if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) @@ -909,7 +904,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); } else if (pngMetadata.TransparentRgb24.HasValue) { @@ -918,7 +913,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); } } else if (pngMetadata.ColorType == PngColorType.Grayscale) @@ -926,13 +921,13 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) if (pngMetadata.TransparentL16.HasValue && this.use16Bit) { BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } else if (pngMetadata.TransparentL8.HasValue) { alpha.Clear(); alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } } } @@ -1173,12 +1168,14 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data) /// The of the data to write. private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length) { - BinaryPrimitives.WriteInt32BigEndian(this.buffer, length); - BinaryPrimitives.WriteUInt32BigEndian(this.buffer.AsSpan(4, 4), (uint)type); + Span buffer = stackalloc byte[8]; + + BinaryPrimitives.WriteInt32BigEndian(buffer, length); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)type); - stream.Write(this.buffer, 0, 8); + stream.Write(buffer); - uint crc = Crc32.Calculate(this.buffer.AsSpan(4, 4)); // Write the type buffer + uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer if (data.Length > 0 && length > 0) { @@ -1187,9 +1184,9 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o crc = Crc32.Calculate(crc, data.Slice(offset, length)); } - BinaryPrimitives.WriteUInt32BigEndian(this.buffer, crc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, crc); - stream.Write(this.buffer, 0, 4); // write the crc + stream.Write(buffer, 0, 4); // write the crc } /// @@ -1412,4 +1409,11 @@ private static PngBitDepth SuggestBitDepth() Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16, _ => PngBitDepth.Bit8 }; + + private unsafe struct ScratchBuffer + { + private fixed byte scratch[16]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 16); + } } From 858a8485b741dd5b2cb790be2e7f37881011e17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:36:15 +0100 Subject: [PATCH 04/10] Reduced intermediate allocations: Jpeg --- .../Jpeg/Components/Decoder/AdobeMarker.cs | 2 +- .../Jpeg/Components/Decoder/JFifMarker.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 146 ++++----- .../Formats/Jpeg/JpegEncoderCore.cs | 289 +++++++++--------- 4 files changed, 231 insertions(+), 208 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs index c9ee55cd77..cf2369b2cb 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs @@ -62,7 +62,7 @@ private AdobeMarker(short dctEncodeVersion, short app14Flags0, short app14Flags1 /// /// The byte array containing metadata to parse. /// The marker to return. - public static bool TryParse(byte[] bytes, out AdobeMarker marker) + public static bool TryParse(ReadOnlySpan bytes, out AdobeMarker marker) { if (ProfileResolver.IsProfile(bytes, ProfileResolver.AdobeMarker)) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs index e13b26f9a9..153dc8a03e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs @@ -69,7 +69,7 @@ private JFifMarker(byte majorVersion, byte minorVersion, byte densityUnits, shor /// /// The byte array containing metadata to parse. /// The marker to return. - public static bool TryParse(byte[] bytes, out JFifMarker marker) + public static bool TryParse(ReadOnlySpan bytes, out JFifMarker marker) { if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker)) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 45029f9459..3c383e7766 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -27,21 +27,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals { - /// - /// The only supported precision - /// - private readonly byte[] supportedPrecisions = { 8, 12 }; - - /// - /// The buffer used to temporarily store bytes read from the stream. - /// - private readonly byte[] temp = new byte[2 * 16 * 4]; - - /// - /// The buffer used to read markers from the stream. - /// - private readonly byte[] markerBuffer = new byte[2]; - /// /// Whether the image has an EXIF marker. /// @@ -139,6 +124,12 @@ public JpegDecoderCore(JpegDecoderOptions options) this.skipMetadata = options.GeneralOptions.SkipMetadata; } + /// + /// The only supported precision + /// + // Refers to assembly's static data segment, no allocation occurs. + private static ReadOnlySpan SupportedPrecisions => new byte[] { 8, 12 }; + /// public DecoderOptions Options { get; } @@ -257,24 +248,26 @@ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) using MemoryStream ms = new(tableBytes); using BufferedReadStream stream = new(this.configuration, ms); + Span markerBuffer = stackalloc byte[2]; + // Check for the Start Of Image marker. - int bytesRead = stream.Read(this.markerBuffer, 0, 2); - JpegFileMarker fileMarker = new(this.markerBuffer[1], 0); + int bytesRead = stream.Read(markerBuffer); + JpegFileMarker fileMarker = new(markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); } // Read next marker. - bytesRead = stream.Read(this.markerBuffer, 0, 2); - fileMarker = new JpegFileMarker(this.markerBuffer[1], (int)stream.Position - 2); + bytesRead = stream.Read(markerBuffer); + fileMarker = new JpegFileMarker(markerBuffer[1], (int)stream.Position - 2); while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) { if (!fileMarker.Invalid) { // Get the marker length. - int markerContentByteSize = this.ReadUint16(stream) - 2; + int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; // Check whether the stream actually has enough bytes to read // markerContentByteSize is always positive so we cast @@ -297,7 +290,7 @@ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DRI: - this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer); break; case JpegConstants.Markers.EOI: return; @@ -305,13 +298,13 @@ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) } // Read next marker. - bytesRead = stream.Read(this.markerBuffer, 0, 2); + bytesRead = stream.Read(markerBuffer); if (bytesRead != 2) { JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); } - fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); + fileMarker = new JpegFileMarker(markerBuffer[1], 0); } } @@ -329,9 +322,11 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC this.Metadata = new ImageMetadata(); + Span markerBuffer = stackalloc byte[2]; + // Check for the Start Of Image marker. - stream.Read(this.markerBuffer, 0, 2); - JpegFileMarker fileMarker = new(this.markerBuffer[1], 0); + stream.Read(markerBuffer); + JpegFileMarker fileMarker = new(markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); @@ -349,7 +344,7 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC if (!fileMarker.Invalid) { // Get the marker length. - int markerContentByteSize = this.ReadUint16(stream) - 2; + int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; // Check whether stream actually has enough bytes to read // markerContentByteSize is always positive so we cast @@ -446,7 +441,7 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC } else { - this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer); } break; @@ -755,8 +750,10 @@ private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remai return; } - stream.Read(this.temp, 0, JFifMarker.Length); - if (!JFifMarker.TryParse(this.temp, out this.jFif)) + Span temp = stackalloc byte[2 * 16 * 4]; + + stream.Read(temp, 0, JFifMarker.Length); + if (!JFifMarker.TryParse(temp, out this.jFif)) { JpegThrowHelper.ThrowNotSupportedException("Unknown App0 Marker - Expected JFIF."); } @@ -796,11 +793,13 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining) JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); } + Span temp = stackalloc byte[2 * 16 * 4]; + // XMP marker is the longer then the EXIF marker, so first try read the EXIF marker bytes. - stream.Read(this.temp, 0, exifMarkerLength); + stream.Read(temp, 0, exifMarkerLength); remaining -= exifMarkerLength; - if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) + if (ProfileResolver.IsProfile(temp, ProfileResolver.ExifMarker)) { this.hasExif = true; byte[] profile = new byte[remaining]; @@ -819,7 +818,7 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining) remaining = 0; } - if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker[..exifMarkerLength])) + if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength])) { const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength; if (remaining < remainingXmpMarkerBytes || this.skipMetadata) @@ -829,9 +828,9 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining) return; } - stream.Read(this.temp, exifMarkerLength, remainingXmpMarkerBytes); + stream.Read(temp, exifMarkerLength, remainingXmpMarkerBytes); remaining -= remainingXmpMarkerBytes; - if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker)) + if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker)) { this.hasXmp = true; byte[] profile = new byte[remaining]; @@ -870,8 +869,8 @@ private void ProcessApp2Marker(BufferedReadStream stream, int remaining) return; } - byte[] identifier = new byte[icclength]; - stream.Read(identifier, 0, icclength); + Span identifier = stackalloc byte[icclength]; + stream.Read(identifier); remaining -= icclength; // We have read it by this point if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) @@ -911,13 +910,13 @@ private void ProcessApp13Marker(BufferedReadStream stream, int remaining) return; } - stream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); + Span temp = stackalloc byte[2 * 16 * 4]; + stream.Read(temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length; - if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker)) + if (ProfileResolver.IsProfile(temp, ProfileResolver.AdobePhotoshopApp13Marker)) { - byte[] resourceBlockData = new byte[remaining]; - stream.Read(resourceBlockData, 0, remaining); - Span blockDataSpan = resourceBlockData.AsSpan(); + Span blockDataSpan = remaining <= 128 ? stackalloc byte[remaining] : new byte[remaining]; + stream.Read(blockDataSpan); while (blockDataSpan.Length > 12) { @@ -1047,10 +1046,12 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining) return; } - stream.Read(this.temp, 0, markerLength); + Span temp = stackalloc byte[2 * 16 * 4]; + + stream.Read(temp, 0, markerLength); remaining -= markerLength; - if (AdobeMarker.TryParse(this.temp, out this.adobe)) + if (AdobeMarker.TryParse(temp, out this.adobe)) { this.hasAdobeMarker = true; } @@ -1072,6 +1073,7 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining) private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + Span temp = stackalloc byte[2 * 16 * 4]; while (remaining > 0) { @@ -1102,13 +1104,13 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } - stream.Read(this.temp, 0, 64); + stream.Read(temp, 0, 64); remaining -= 64; // Parsing quantization table & saving it in natural order for (int j = 0; j < 64; j++) { - table[ZigZag.ZigZagOrder[j]] = this.temp[j]; + table[ZigZag.ZigZagOrder[j]] = temp[j]; } break; @@ -1121,13 +1123,13 @@ private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, in JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } - stream.Read(this.temp, 0, 128); + stream.Read(temp, 0, 128); remaining -= 128; // Parsing quantization table & saving it in natural order for (int j = 0; j < 64; j++) { - table[ZigZag.ZigZagOrder[j]] = (this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]; + table[ZigZag.ZigZagOrder[j]] = (temp[2 * j] << 8) | temp[(2 * j) + 1]; } break; @@ -1174,28 +1176,30 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); } + Span temp = stackalloc byte[2 * 16 * 4]; + // Read initial marker definitions. const int length = 6; - int bytesRead = stream.Read(this.temp, 0, length); + int bytesRead = stream.Read(temp, 0, length); if (bytesRead != length) { JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data."); } // 1 byte: Bits/sample precision. - byte precision = this.temp[0]; + byte precision = temp[0]; // Validate: only 8-bit and 12-bit precisions are supported. - if (Array.IndexOf(this.supportedPrecisions, precision) == -1) + if (SupportedPrecisions.IndexOf(precision) < 0) { JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision is supported."); } // 2 byte: Height - int frameHeight = (this.temp[1] << 8) | this.temp[2]; + int frameHeight = (temp[1] << 8) | temp[2]; // 2 byte: Width - int frameWidth = (this.temp[3] << 8) | this.temp[4]; + int frameWidth = (temp[3] << 8) | temp[4]; // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that). if (frameHeight == 0 || frameWidth == 0) @@ -1204,7 +1208,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, } // 1 byte: Number of components. - byte componentCount = this.temp[5]; + byte componentCount = temp[5]; // Validate: componentCount more than 4 can lead to a buffer overflow during stream // reading so we must limit it to 4. @@ -1227,7 +1231,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, } // components*3 bytes: component data - stream.Read(this.temp, 0, remaining); + stream.Read(temp, 0, remaining); // No need to pool this. They max out at 4 this.Frame.ComponentIds = new byte[componentCount]; @@ -1240,10 +1244,10 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, for (int i = 0; i < this.Frame.Components.Length; i++) { // 1 byte: component identifier - byte componentId = this.temp[index]; + byte componentId = temp[index]; // 1 byte: component sampling factors - byte hv = this.temp[index + 1]; + byte hv = temp[index + 1]; int h = (hv >> 4) & 15; int v = hv & 15; @@ -1270,7 +1274,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, } // 1 byte: quantization table destination selector - byte quantTableIndex = this.temp[index + 2]; + byte quantTableIndex = temp[index + 2]; // Validate: 0-3 range if (quantTableIndex > 3) @@ -1379,7 +1383,8 @@ private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int rem /// /// The input stream. /// The remaining bytes in the segment block. - private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining) + /// Scratch buffer. + private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining, Span markerBuffer) { if (remaining != 2) { @@ -1388,7 +1393,7 @@ private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int r // Save the reset interval, because it can come before or after the SOF marker. // If the reset interval comes after the SOF marker, the scanDecoder has not been created. - this.resetInterval = this.ReadUint16(stream); + this.resetInterval = ReadUint16(stream, markerBuffer); if (this.scanDecoder != null) { @@ -1425,14 +1430,16 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining); } + Span temp = stackalloc byte[2 * 16 * 4]; + // selectorsCount*2 bytes: component index + huffman tables indices - stream.Read(this.temp, 0, selectorsBytes); + stream.Read(temp, 0, selectorsBytes); this.Frame.Interleaved = this.Frame.ComponentCount == selectorsCount; for (int i = 0; i < selectorsBytes; i += 2) { // 1 byte: Component id - int componentSelectorId = this.temp[i]; + int componentSelectorId = temp[i]; int componentIndex = -1; for (int j = 0; j < this.Frame.ComponentIds.Length; j++) @@ -1459,7 +1466,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) // 1 byte: Huffman table selectors. // 4 bits - dc // 4 bits - ac - int tableSpec = this.temp[i + 1]; + int tableSpec = temp[i + 1]; int dcTableIndex = tableSpec >> 4; int acTableIndex = tableSpec & 15; @@ -1475,17 +1482,17 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) } // 3 bytes: Progressive scan decoding data. - int bytesRead = stream.Read(this.temp, 0, 3); + int bytesRead = stream.Read(temp, 0, 3); if (bytesRead != 3) { JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data"); } - this.scanDecoder.SpectralStart = this.temp[0]; + this.scanDecoder.SpectralStart = temp[0]; - this.scanDecoder.SpectralEnd = this.temp[1]; + this.scanDecoder.SpectralEnd = temp[1]; - int successiveApproximation = this.temp[2]; + int successiveApproximation = temp[2]; this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4; this.scanDecoder.SuccessiveLow = successiveApproximation & 15; @@ -1501,16 +1508,17 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining) /// Reads a from the stream advancing it by two bytes. /// /// The input stream. + /// The scratch buffer used for reading from the stream. /// The [MethodImpl(InliningOptions.ShortMethod)] - private ushort ReadUint16(BufferedReadStream stream) + private static ushort ReadUint16(BufferedReadStream stream, Span markerBuffer) { - int bytesRead = stream.Read(this.markerBuffer, 0, 2); + int bytesRead = stream.Read(markerBuffer, 0, 2); if (bytesRead != 2) { JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort."); } - return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); + return BinaryPrimitives.ReadUInt16BigEndian(markerBuffer); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 1d06333e30..95f7fde32c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -25,11 +25,6 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs(); - /// - /// A scratch buffer to reduce allocations. - /// - private readonly byte[] buffer = new byte[20]; - private readonly JpegEncoder encoder; /// @@ -67,6 +62,7 @@ public void Encode(Image image, Stream stream, CancellationToken cancellationToken.ThrowIfCancellationRequested(); this.outputStream = stream; + Span buffer = stackalloc byte[20]; ImageMetadata metadata = image.Metadata; JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); @@ -76,39 +72,39 @@ public void Encode(Image image, Stream stream, CancellationToken using JpegFrame frame = new(image, frameConfig, interleaved); // Write the Start Of Image marker. - this.WriteStartOfImage(); + this.WriteStartOfImage(buffer); // Write APP0 marker if (frameConfig.AdobeColorTransformMarkerFlag is null) { - this.WriteJfifApplicationHeader(metadata); + this.WriteJfifApplicationHeader(metadata, buffer); } // Write APP14 marker with adobe color extension else { - this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value); + this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value, buffer); } // Write Exif, XMP, ICC and IPTC profiles - this.WriteProfiles(metadata); + this.WriteProfiles(metadata, buffer); // Write the image dimensions. - this.WriteStartOfFrame(image.Width, image.Height, frameConfig); + this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); // Write the Huffman tables. HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); - this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder); + this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); // Write the quantization tables. - this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata); + this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); // Write scans with actual pixel data using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables); - this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken); + this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); // Write the End Of Image marker. - this.WriteEndOfImageMarker(); + this.WriteEndOfImageMarker(buffer); stream.Flush(); } @@ -116,58 +112,59 @@ public void Encode(Image image, Stream stream, CancellationToken /// /// Write the start of image marker. /// - private void WriteStartOfImage() + private void WriteStartOfImage(Span buffer) { // Markers are always prefixed with 0xff. - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.SOI; + buffer[1] = JpegConstants.Markers.SOI; + buffer[0] = JpegConstants.Markers.XFF; - this.outputStream.Write(this.buffer, 0, 2); + this.outputStream.Write(buffer, 0, 2); } /// /// Writes the application header containing the JFIF identifier plus extra data. /// /// The image metadata. - private void WriteJfifApplicationHeader(ImageMetadata meta) + /// Temporary buffer. + private void WriteJfifApplicationHeader(ImageMetadata meta, Span buffer) { - // Write the JFIF headers - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker - this.buffer[2] = 0x00; - this.buffer[3] = 0x10; - this.buffer[4] = 0x4a; // J - this.buffer[5] = 0x46; // F - this.buffer[6] = 0x49; // I - this.buffer[7] = 0x46; // F - this.buffer[8] = 0x00; // = "JFIF",'\0' - this.buffer[9] = 0x01; // versionhi - this.buffer[10] = 0x01; // versionlo + // Write the JFIF headers (highest index first to avoid additional bound checks) + buffer[10] = 0x01; // versionlo + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = JpegConstants.Markers.APP0; // Application Marker + buffer[2] = 0x00; + buffer[3] = 0x10; + buffer[4] = 0x4a; // J + buffer[5] = 0x46; // F + buffer[6] = 0x49; // I + buffer[7] = 0x46; // F + buffer[8] = 0x00; // = "JFIF",'\0' + buffer[9] = 0x01; // versionhi // Resolution. Big Endian - Span hResolution = this.buffer.AsSpan(12, 2); - Span vResolution = this.buffer.AsSpan(14, 2); + Span hResolution = buffer.Slice(12, 2); + Span vResolution = buffer.Slice(14, 2); if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) { // Scale down to PPI - this.buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits + buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution))); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); } else { // We can simply pass the value. - this.buffer[11] = (byte)meta.ResolutionUnits; // xyunits + buffer[11] = (byte)meta.ResolutionUnits; // xyunits BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(meta.HorizontalResolution)); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); } // No thumbnail - this.buffer[16] = 0x00; // Thumbnail width - this.buffer[17] = 0x00; // Thumbnail height + buffer[17] = 0x00; // Thumbnail height + buffer[16] = 0x00; // Thumbnail width - this.outputStream.Write(this.buffer, 0, 18); + this.outputStream.Write(buffer, 0, 18); } /// @@ -175,8 +172,9 @@ private void WriteJfifApplicationHeader(ImageMetadata meta) /// /// The table configuration. /// The scan encoder. + /// Temporary buffer. /// is . - private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder) + private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder, Span buffer) { if (tableConfigs is null) { @@ -190,7 +188,7 @@ private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, Huf markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length; } - this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen); + this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen, buffer); for (int i = 0; i < tableConfigs.Length; i++) { JpegHuffmanTableConfig tableConfig = tableConfigs[i]; @@ -208,37 +206,39 @@ private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, Huf /// Writes the APP14 marker to indicate the image is in RGB color space. /// /// The color transform byte. - private void WriteApp14Marker(byte colorTransform) + /// Temporary buffer. + private void WriteApp14Marker(byte colorTransform, Span buffer) { - this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length); + this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length, buffer); - // Identifier: ASCII "Adobe". - this.buffer[0] = 0x41; - this.buffer[1] = 0x64; - this.buffer[2] = 0x6F; - this.buffer[3] = 0x62; - this.buffer[4] = 0x65; + // Identifier: ASCII "Adobe" (highest index first to avoid additional bound checks). + buffer[4] = 0x65; + buffer[0] = 0x41; + buffer[1] = 0x64; + buffer[2] = 0x6F; + buffer[3] = 0x62; // Version, currently 100. - BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(5, 2), 100); + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(5, 2), 100); // Flags0 - BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(7, 2), 0); + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(7, 2), 0); // Flags1 - BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0); + BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(9, 2), 0); // Color transform byte - this.buffer[11] = colorTransform; + buffer[11] = colorTransform; - this.outputStream.Write(this.buffer.AsSpan(0, 12)); + this.outputStream.Write(buffer.Slice(0, 12)); } /// /// Writes the EXIF profile. /// /// The exif profile. - private void WriteExifProfile(ExifProfile exifProfile) + /// Temporary buffer. + private void WriteExifProfile(ExifProfile exifProfile, Span buffer) { if (exifProfile is null || exifProfile.Values.Count == 0) { @@ -262,7 +262,7 @@ private void WriteExifProfile(ExifProfile exifProfile) int app1Length = bytesToWrite + 2; // Write the app marker, EXIF marker, and data - this.WriteApp1Header(app1Length); + this.WriteApp1Header(app1Length, buffer); this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength); remaining -= bytesToWrite; @@ -273,7 +273,7 @@ private void WriteExifProfile(ExifProfile exifProfile) bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining; app1Length = bytesToWrite + 2 + exifMarkerLength; - this.WriteApp1Header(app1Length); + this.WriteApp1Header(app1Length, buffer); // Write Exif00 marker this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); @@ -289,10 +289,11 @@ private void WriteExifProfile(ExifProfile exifProfile) /// Writes the IPTC metadata. /// /// The iptc metadata to write. + /// Temporary buffer. /// /// Thrown if the IPTC profile size exceeds the limit of 65533 bytes. /// - private void WriteIptcProfile(IptcProfile iptcProfile) + private void WriteIptcProfile(IptcProfile iptcProfile, Span buffer) { const int maxBytes = 65533; if (iptcProfile is null || !iptcProfile.Values.Any()) @@ -316,14 +317,14 @@ private void WriteIptcProfile(IptcProfile iptcProfile) Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker.Length + Components.Decoder.ProfileResolver.AdobeIptcMarker.Length + 2 + 4 + data.Length; - this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13); + this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13, buffer); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker); this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even) this.outputStream.WriteByte(0); - BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length); - this.outputStream.Write(this.buffer, 0, 4); + BinaryPrimitives.WriteInt32BigEndian(buffer, data.Length); + this.outputStream.Write(buffer, 0, 4); this.outputStream.Write(data, 0, data.Length); } @@ -331,10 +332,11 @@ private void WriteIptcProfile(IptcProfile iptcProfile) /// Writes the XMP metadata. /// /// The XMP metadata to write. + /// Temporary buffer. /// /// Thrown if the XMP profile size exceeds the limit of 65533 bytes. /// - private void WriteXmpProfile(XmpProfile xmpProfile) + private void WriteXmpProfile(XmpProfile xmpProfile, Span buffer) { if (xmpProfile is null) { @@ -367,7 +369,7 @@ private void WriteXmpProfile(XmpProfile xmpProfile) dataLength -= length; int app1Length = 2 + Components.Decoder.ProfileResolver.XmpMarker.Length + length; - this.WriteApp1Header(app1Length); + this.WriteApp1Header(app1Length, buffer); this.outputStream.Write(Components.Decoder.ProfileResolver.XmpMarker); this.outputStream.Write(data, offset, length); @@ -379,32 +381,35 @@ private void WriteXmpProfile(XmpProfile xmpProfile) /// Writes the App1 header. /// /// The length of the data the app1 marker contains. - private void WriteApp1Header(int app1Length) - => this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1); + /// Temporary buffer. + private void WriteApp1Header(int app1Length, Span buffer) + => this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1, buffer); /// /// Writes a AppX header. /// /// The length of the data the app marker contains. /// The app marker to write. - private void WriteAppHeader(int length, byte appMarker) + /// Temporary buffer. + private void WriteAppHeader(int length, byte appMarker, Span buffer) { - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = appMarker; - this.buffer[2] = (byte)((length >> 8) & 0xFF); - this.buffer[3] = (byte)(length & 0xFF); + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = appMarker; + buffer[2] = (byte)((length >> 8) & 0xFF); + buffer[3] = (byte)(length & 0xFF); - this.outputStream.Write(this.buffer, 0, 4); + this.outputStream.Write(buffer, 0, 4); } /// /// Writes the ICC profile. /// /// The ICC profile to write. + /// Temporary buffer. /// /// Thrown if any of the ICC profiles size exceeds the limit. /// - private void WriteIccProfile(IccProfile iccProfile) + private void WriteIccProfile(IccProfile iccProfile, Span buffer) { if (iccProfile is null) { @@ -446,30 +451,31 @@ private void WriteIccProfile(IccProfile iccProfile) dataLength -= length; - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.APP2; // Application Marker + buffer[0] = JpegConstants.Markers.XFF; + buffer[1] = JpegConstants.Markers.APP2; // Application Marker int markerLength = length + 16; - this.buffer[2] = (byte)((markerLength >> 8) & 0xFF); - this.buffer[3] = (byte)(markerLength & 0xFF); - - this.outputStream.Write(this.buffer, 0, 4); - - this.buffer[0] = (byte)'I'; - this.buffer[1] = (byte)'C'; - this.buffer[2] = (byte)'C'; - this.buffer[3] = (byte)'_'; - this.buffer[4] = (byte)'P'; - this.buffer[5] = (byte)'R'; - this.buffer[6] = (byte)'O'; - this.buffer[7] = (byte)'F'; - this.buffer[8] = (byte)'I'; - this.buffer[9] = (byte)'L'; - this.buffer[10] = (byte)'E'; - this.buffer[11] = 0x00; - this.buffer[12] = (byte)current; // The position within the collection. - this.buffer[13] = (byte)count; // The total number of profiles. - - this.outputStream.Write(this.buffer, 0, iccOverheadLength); + buffer[2] = (byte)((markerLength >> 8) & 0xFF); + buffer[3] = (byte)(markerLength & 0xFF); + + this.outputStream.Write(buffer, 0, 4); + + // We write the highest index first, to have only one bound check. + buffer[13] = (byte)count; // The total number of profiles. + buffer[12] = (byte)current; // The position within the collection. + buffer[11] = 0x00; + buffer[0] = (byte)'I'; + buffer[1] = (byte)'C'; + buffer[2] = (byte)'C'; + buffer[3] = (byte)'_'; + buffer[4] = (byte)'P'; + buffer[5] = (byte)'R'; + buffer[6] = (byte)'O'; + buffer[7] = (byte)'F'; + buffer[8] = (byte)'I'; + buffer[9] = (byte)'L'; + buffer[10] = (byte)'E'; + + this.outputStream.Write(buffer, 0, iccOverheadLength); this.outputStream.Write(data, offset, length); current++; @@ -481,7 +487,8 @@ private void WriteIccProfile(IccProfile iccProfile) /// Writes the metadata profiles to the image. /// /// The image metadata. - private void WriteProfiles(ImageMetadata metadata) + /// Temporary buffer. + private void WriteProfiles(ImageMetadata metadata, Span buffer) { if (metadata is null) { @@ -494,10 +501,10 @@ private void WriteProfiles(ImageMetadata metadata) // - APP2 ICC // - APP13 IPTC metadata.SyncProfiles(); - this.WriteExifProfile(metadata.ExifProfile); - this.WriteXmpProfile(metadata.XmpProfile); - this.WriteIccProfile(metadata.IccProfile); - this.WriteIptcProfile(metadata.IptcProfile); + this.WriteExifProfile(metadata.ExifProfile, buffer); + this.WriteXmpProfile(metadata.XmpProfile, buffer); + this.WriteIccProfile(metadata.IccProfile, buffer); + this.WriteIptcProfile(metadata.IptcProfile, buffer); } /// @@ -506,25 +513,26 @@ private void WriteProfiles(ImageMetadata metadata) /// The frame width. /// The frame height. /// The frame configuration. - private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) + /// Temporary buffer. + private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Span buffer) { JpegComponentConfig[] components = frame.Components; // Length (high byte, low byte), 8 + components * 3. int markerlen = 8 + (3 * components.Length); - this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen); - this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported - this.buffer[1] = (byte)(height >> 8); - this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported - this.buffer[3] = (byte)(width >> 8); - this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported - this.buffer[5] = (byte)components.Length; + this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer); + buffer[5] = (byte)components.Length; + buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported + buffer[1] = (byte)(height >> 8); + buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported + buffer[3] = (byte)(width >> 8); + buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported // Components data for (int i = 0; i < components.Length; i++) { int i3 = 3 * i; - Span bufferSpan = this.buffer.AsSpan(i3 + 6, 3); + Span bufferSpan = buffer.Slice(i3 + 6, 3); // Quantization table selector bufferSpan[2] = (byte)components[i].QuantizatioTableIndex; @@ -538,14 +546,15 @@ private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) bufferSpan[0] = components[i].Id; } - this.outputStream.Write(this.buffer, 0, (3 * (components.Length - 1)) + 9); + this.outputStream.Write(buffer, 0, (3 * (components.Length - 1)) + 9); } /// /// Writes the StartOfScan marker. /// /// The collecction of component configuration items. - private void WriteStartOfScan(Span components) + /// Temporary buffer. + private void WriteStartOfScan(Span components, Span buffer) { // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // - the marker length "\x00\x0c", @@ -556,14 +565,14 @@ private void WriteStartOfScan(Span components) // - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al) // should be 0x00, 0x3f, 0x00<<4 | 0x00. - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.SOS; + buffer[1] = JpegConstants.Markers.SOS; + buffer[0] = JpegConstants.Markers.XFF; // Length (high byte, low byte), must be 6 + 2 * (number of components in scan) int sosSize = 6 + (2 * components.Length); - this.buffer[2] = 0x00; - this.buffer[3] = (byte)sosSize; - this.buffer[4] = (byte)components.Length; // Number of components in a scan + buffer[4] = (byte)components.Length; // Number of components in a scan + buffer[3] = (byte)sosSize; + buffer[2] = 0x00; // Components data for (int i = 0; i < components.Length; i++) @@ -571,27 +580,28 @@ private void WriteStartOfScan(Span components) int i2 = 2 * i; // Id - this.buffer[i2 + 5] = components[i].Id; + buffer[i2 + 5] = components[i].Id; // Table selectors int tableSelectors = (components[i].DcTableSelector << 4) | components[i].AcTableSelector; - this.buffer[i2 + 6] = (byte)tableSelectors; + buffer[i2 + 6] = (byte)tableSelectors; } - this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection. - this.buffer[sosSize] = 0x3f; // Se - End of spectral selection. - this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) - this.outputStream.Write(this.buffer, 0, sosSize + 2); + buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection. + buffer[sosSize] = 0x3f; // Se - End of spectral selection. + buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) + this.outputStream.Write(buffer, 0, sosSize + 2); } /// /// Writes the EndOfImage marker. /// - private void WriteEndOfImageMarker() + /// Temporary buffer. + private void WriteEndOfImageMarker(Span buffer) { - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = JpegConstants.Markers.EOI; - this.outputStream.Write(this.buffer, 0, 2); + buffer[1] = JpegConstants.Markers.EOI; + buffer[0] = JpegConstants.Markers.XFF; + this.outputStream.Write(buffer, 0, 2); } /// @@ -602,12 +612,14 @@ private void WriteEndOfImageMarker() /// The frame configuration. /// The spectral converter. /// The scan encoder. + /// Temporary buffer. /// The cancellation token. private void WriteHuffmanScans( JpegFrame frame, JpegFrameConfig frameConfig, SpectralConverter spectralConverter, HuffmanScanEncoder encoder, + Span buffer, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -615,14 +627,14 @@ private void WriteHuffmanScans( { frame.AllocateComponents(fullScan: false); - this.WriteStartOfScan(frameConfig.Components); + this.WriteStartOfScan(frameConfig.Components, buffer); encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken); } else if (frame.Interleaved) { frame.AllocateComponents(fullScan: false); - this.WriteStartOfScan(frameConfig.Components); + this.WriteStartOfScan(frameConfig.Components, buffer); encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken); } else @@ -633,7 +645,7 @@ private void WriteHuffmanScans( Span components = frameConfig.Components; for (int i = 0; i < frame.Components.Length; i++) { - this.WriteStartOfScan(components.Slice(i, 1)); + this.WriteStartOfScan(components.Slice(i, 1), buffer); encoder.EncodeScanBaseline(frame.Components[i], cancellationToken); } } @@ -644,14 +656,16 @@ private void WriteHuffmanScans( /// /// The marker to write. /// The marker length. - private void WriteMarkerHeader(byte marker, int length) + /// Temporary buffer. + private void WriteMarkerHeader(byte marker, int length, Span buffer) { // Markers are always prefixed with 0xff. - this.buffer[0] = JpegConstants.Markers.XFF; - this.buffer[1] = marker; - this.buffer[2] = (byte)(length >> 8); - this.buffer[3] = (byte)(length & 0xff); - this.outputStream.Write(this.buffer, 0, 4); + buffer[3] = (byte)(length & 0xff); + buffer[2] = (byte)(length >> 8); + buffer[1] = marker; + buffer[0] = JpegConstants.Markers.XFF; + + this.outputStream.Write(buffer, 0, 4); } /// @@ -668,15 +682,16 @@ private void WriteMarkerHeader(byte marker, int length) /// Quantization tables configs. /// Optional quality value from the options. /// Jpeg metadata instance. - private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata) + /// Temporary buffer. + private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata, Span tmpBuffer) { int dataLen = configs.Length * (1 + Block8x8.Size); // Marker + quantization table lengths. int markerlen = 2 + dataLen; - this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); + this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen, tmpBuffer); - byte[] buffer = new byte[dataLen]; + Span buffer = dataLen <= 256 ? stackalloc byte[dataLen] : new byte[dataLen]; int offset = 0; Block8x8F workspaceBlock = default; From 1d3ae0ed9dc3ad0a20efb6e2650e2d08b24624bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:36:30 +0100 Subject: [PATCH 05/10] Reduced intermediate allocations: Gif --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 29 +++++++++++------- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 32 ++++++++++---------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 6ff2723ddd..efde4e9aa8 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -22,7 +22,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// /// The temp buffer used to reduce allocations. /// - private readonly byte[] buffer = new byte[16]; + private ScratchBuffer buffer; // mutable struct, don't make readonly /// /// The global color table. @@ -249,13 +249,13 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat /// The containing image data. private void ReadGraphicalControlExtension(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer, 0, 6); + int bytesRead = stream.Read(this.buffer.Span, 0, 6); if (bytesRead != 6) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); } - this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer); + this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer.Span); } /// @@ -264,13 +264,13 @@ private void ReadGraphicalControlExtension(BufferedReadStream stream) /// The containing image data. private void ReadImageDescriptor(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer, 0, 9); + int bytesRead = stream.Read(this.buffer.Span, 0, 9); if (bytesRead != 9) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); } - this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); + this.imageDescriptor = GifImageDescriptor.Parse(this.buffer.Span); if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) { GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); @@ -283,13 +283,13 @@ private void ReadImageDescriptor(BufferedReadStream stream) /// The containing image data. private void ReadLogicalScreenDescriptor(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer, 0, 7); + int bytesRead = stream.Read(this.buffer.Span, 0, 7); if (bytesRead != 7) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); } - this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); + this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer.Span); } /// @@ -306,8 +306,8 @@ private void ReadApplicationExtension(BufferedReadStream stream) long position = stream.Position; if (appLength == GifConstants.ApplicationBlockSize) { - stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); - bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); + stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); + bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp && !this.skipMetadata) { GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); @@ -331,8 +331,8 @@ private void ReadApplicationExtension(BufferedReadStream stream) // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) { - stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount; stream.Skip(1); // Skip the terminator. return; } @@ -762,4 +762,11 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s } } } + + private unsafe struct ScratchBuffer + { + private fixed byte scratch[16]; + + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 16); + } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index f736da78dd..c01cc78ef0 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -28,11 +28,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// private readonly Configuration configuration; - /// - /// A reusable buffer used to reduce allocations. - /// - private readonly byte[] buffer = new byte[20]; - /// /// Whether to skip metadata during encode. /// @@ -324,9 +319,10 @@ private void WriteLogicalScreenDescriptor( backgroundColorIndex: unchecked((byte)transparencyIndex), ratio); - descriptor.WriteTo(this.buffer); + Span buffer = stackalloc byte[20]; + descriptor.WriteTo(buffer); - stream.Write(this.buffer, 0, GifLogicalScreenDescriptor.Size); + stream.Write(buffer, 0, GifLogicalScreenDescriptor.Size); } /// @@ -365,12 +361,14 @@ private void WriteComments(GifMetadata metadata, Stream stream) return; } + Span buffer = stackalloc byte[2]; + for (int i = 0; i < metadata.Comments.Count; i++) { string comment = metadata.Comments[i]; - this.buffer[0] = GifConstants.ExtensionIntroducer; - this.buffer[1] = GifConstants.CommentLabel; - stream.Write(this.buffer, 0, 2); + buffer[1] = GifConstants.CommentLabel; + buffer[0] = GifConstants.ExtensionIntroducer; + stream.Write(buffer); // Comment will be stored in chunks of 255 bytes, if it exceeds this size. ReadOnlySpan commentSpan = comment.AsSpan(); @@ -437,22 +435,23 @@ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int trans private void WriteExtension(TGifExtension extension, Stream stream) where TGifExtension : struct, IGifExtension { - IMemoryOwner? owner = null; - Span extensionBuffer; int extensionSize = extension.ContentLength; if (extensionSize == 0) { return; } - else if (extensionSize > this.buffer.Length - 3) + + IMemoryOwner? owner = null; + Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation + if (extensionSize > 128) { owner = this.memoryAllocator.Allocate(extensionSize + 3); extensionBuffer = owner.GetSpan(); } else { - extensionBuffer = this.buffer; + extensionBuffer = stackalloc byte[extensionSize + 3]; } extensionBuffer[0] = GifConstants.ExtensionIntroducer; @@ -489,9 +488,10 @@ private void WriteImageDescriptor(ImageFrame image, bool hasColo height: (ushort)image.Height, packed: packedValue); - descriptor.WriteTo(this.buffer); + Span buffer = stackalloc byte[20]; + descriptor.WriteTo(buffer); - stream.Write(this.buffer, 0, GifImageDescriptor.Size); + stream.Write(buffer, 0, GifImageDescriptor.Size); } /// From 3c3479ee1218f8acc2ce4a468fb9159fbeace1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:36:43 +0100 Subject: [PATCH 06/10] Reduced intermediate allocations: Tga --- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 43 ++++++++++---------- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 12 ++---- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e7dca00f79..26e057bff9 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -17,11 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// internal sealed class TgaDecoderCore : IImageDecoderInternals { - /// - /// A scratch buffer to reduce allocations. - /// - private readonly byte[] scratchBuffer = new byte[4]; - /// /// General configuration options. /// @@ -407,6 +402,7 @@ private void ReadBgra16(BufferedReadStream stream, int width, int height bool invertX = InvertX(origin); using IMemoryOwner row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0); Span rowSpan = row.GetSpan(); + Span scratchBuffer = stackalloc byte[2]; for (int y = 0; y < height; y++) { @@ -417,7 +413,7 @@ private void ReadBgra16(BufferedReadStream stream, int width, int height { for (int x = width - 1; x >= 0; x--) { - int bytesRead = stream.Read(this.scratchBuffer, 0, 2); + int bytesRead = stream.Read(scratchBuffer); if (bytesRead != 2) { TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); @@ -425,16 +421,16 @@ private void ReadBgra16(BufferedReadStream stream, int width, int height if (!this.hasAlpha) { - this.scratchBuffer[1] |= 1 << 7; + scratchBuffer[1] |= 1 << 7; } if (this.fileHeader.ImageType == TgaImageType.BlackAndWhite) { - color.FromLa16(Unsafe.As(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); + color.FromLa16(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); } else { - color.FromBgra5551(Unsafe.As(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); + color.FromBgra5551(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); } pixelSpan[x] = color; @@ -484,6 +480,7 @@ private void ReadBgr24(BufferedReadStream stream, int width, int height, bool invertX = InvertX(origin); if (invertX) { + Span scratchBuffer = stackalloc byte[4]; TPixel color = default; for (int y = 0; y < height; y++) { @@ -491,7 +488,7 @@ private void ReadBgr24(BufferedReadStream stream, int width, int height, Span pixelSpan = pixels.DangerousGetRowSpan(newY); for (int x = width - 1; x >= 0; x--) { - this.ReadBgr24Pixel(stream, color, x, pixelSpan); + ReadBgr24Pixel(stream, color, x, pixelSpan, scratchBuffer); } } @@ -558,6 +555,8 @@ private void ReadBgra32(BufferedReadStream stream, int width, int height return; } + Span scratchBuffer = stackalloc byte[4]; + for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); @@ -566,14 +565,14 @@ private void ReadBgra32(BufferedReadStream stream, int width, int height { for (int x = width - 1; x >= 0; x--) { - this.ReadBgra32Pixel(stream, x, color, pixelRow); + this.ReadBgra32Pixel(stream, x, color, pixelRow, scratchBuffer); } } else { for (int x = 0; x < width; x++) { - this.ReadBgra32Pixel(stream, x, color, pixelRow); + this.ReadBgra32Pixel(stream, x, color, pixelRow, scratchBuffer); } } } @@ -687,16 +686,16 @@ private static void ReadL8Pixel(BufferedReadStream stream, TPixel color, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReadBgr24Pixel(BufferedReadStream stream, TPixel color, int x, Span pixelSpan) + private static void ReadBgr24Pixel(BufferedReadStream stream, TPixel color, int x, Span pixelSpan, Span scratchBuffer) where TPixel : unmanaged, IPixel { - int bytesRead = stream.Read(this.scratchBuffer, 0, 3); + int bytesRead = stream.Read(scratchBuffer, 0, 3); if (bytesRead != 3) { TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel"); } - color.FromBgr24(Unsafe.As(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); + color.FromBgr24(Unsafe.As(ref MemoryMarshal.GetReference(scratchBuffer))); pixelSpan[x] = color; } @@ -715,10 +714,10 @@ private void ReadBgr24Row(BufferedReadStream stream, int width, Buffer2D } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReadBgra32Pixel(BufferedReadStream stream, int x, TPixel color, Span pixelRow) + private void ReadBgra32Pixel(BufferedReadStream stream, int x, TPixel color, Span pixelRow, Span scratchBuffer) where TPixel : unmanaged, IPixel { - int bytesRead = stream.Read(this.scratchBuffer, 0, 4); + int bytesRead = stream.Read(scratchBuffer, 0, 4); if (bytesRead != 4) { TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel"); @@ -726,8 +725,8 @@ private void ReadBgra32Pixel(BufferedReadStream stream, int x, TPixel co Guard.NotNull(this.tgaMetadata); - byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : this.scratchBuffer[3]; - color.FromBgra32(new Bgra32(this.scratchBuffer[2], this.scratchBuffer[1], this.scratchBuffer[0], alpha)); + byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : scratchBuffer[3]; + color.FromBgra32(new Bgra32(scratchBuffer[2], scratchBuffer[1], scratchBuffer[0], alpha)); pixelRow[x] = color; } @@ -814,7 +813,7 @@ private static void ReadPalettedBgra32Pixel(BufferedReadStream stream, S private void UncompressRle(BufferedReadStream stream, int width, int height, Span buffer, int bytesPerPixel) { int uncompressedPixels = 0; - Span pixel = this.scratchBuffer.AsSpan(0, bytesPerPixel); + Span pixel = stackalloc byte[bytesPerPixel]; int totalPixels = width * height; while (uncompressedPixels < totalPixels) { @@ -825,7 +824,7 @@ private void UncompressRle(BufferedReadStream stream, int width, int height, Spa if (highBit == 1) { int runLength = runLengthByte & 127; - int bytesRead = stream.Read(pixel, 0, bytesPerPixel); + int bytesRead = stream.Read(pixel); if (bytesRead != bytesPerPixel) { TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); @@ -845,7 +844,7 @@ private void UncompressRle(BufferedReadStream stream, int width, int height, Spa int bufferIdx = uncompressedPixels * bytesPerPixel; for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) { - int bytesRead = stream.Read(pixel, 0, bytesPerPixel); + int bytesRead = stream.Read(pixel); if (bytesRead != bytesPerPixel) { TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index f468ab9ae7..ad63bd356d 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -22,11 +22,6 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals /// private readonly MemoryAllocator memoryAllocator; - /// - /// Reusable buffer for writing data. - /// - private readonly byte[] buffer = new byte[2]; - /// /// The color depth, in number of bits per pixel. /// @@ -221,9 +216,10 @@ private void WritePixel(Stream stream, TPixel currentPixel, Rgba32 color case TgaBitsPerPixel.Pixel16: Bgra5551 bgra5551 = new(color.ToVector4()); - BinaryPrimitives.WriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); - stream.WriteByte(this.buffer[0]); - stream.WriteByte(this.buffer[1]); + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue); + stream.WriteByte(buffer[0]); + stream.WriteByte(buffer[1]); break; From b9b6f72008689a98cf13ce4bfb89fe9a3de2a3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:36:55 +0100 Subject: [PATCH 07/10] Reduced intermediate allocations: Bmp --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 38 +++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 0c1b273f77..863fed359c 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -453,6 +453,7 @@ private void ReadRle24(BufferedReadStream stream, Buffer2D pixel /// Keeps track of rows, which have undefined pixels. private void UncompressRle4(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) { + Span scratchBuffer = stackalloc byte[128]; Span cmd = stackalloc byte[2]; int count = 0; @@ -491,9 +492,9 @@ private void UncompressRle4(BufferedReadStream stream, int w, Span buffer, int max = cmd[1]; int bytesToRead = (int)(((uint)max + 1) / 2); - byte[] run = new byte[bytesToRead]; + Span run = bytesToRead <= 128 ? scratchBuffer.Slice(0, bytesToRead) : new byte[bytesToRead]; - stream.Read(run, 0, run.Length); + stream.Read(run); int idx = 0; for (int i = 0; i < max; i++) @@ -559,6 +560,7 @@ private void UncompressRle4(BufferedReadStream stream, int w, Span buffer, /// Keeps track of rows, which have undefined pixels. private void UncompressRle8(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) { + Span scratchBuffer = stackalloc byte[128]; Span cmd = stackalloc byte[2]; int count = 0; @@ -596,13 +598,13 @@ private void UncompressRle8(BufferedReadStream stream, int w, Span buffer, // Take this number of bytes from the stream as uncompressed data. int length = cmd[1]; - byte[] run = new byte[length]; + Span run = length <= 128 ? scratchBuffer.Slice(0, length) : new byte[length]; - stream.Read(run, 0, run.Length); + stream.Read(run); - run.AsSpan().CopyTo(buffer[count..]); + run.CopyTo(buffer[count..]); - count += run.Length; + count += length; // Absolute mode data is aligned to two-byte word-boundary. int padding = length & 1; @@ -639,6 +641,7 @@ private void UncompressRle8(BufferedReadStream stream, int w, Span buffer, /// Keeps track of rows, which have undefined pixels. private void UncompressRle24(BufferedReadStream stream, int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels) { + Span scratchBuffer = stackalloc byte[128]; Span cmd = stackalloc byte[2]; int uncompressedPixels = 0; @@ -675,17 +678,18 @@ private void UncompressRle24(BufferedReadStream stream, int w, Span buffer // If the second byte > 2, we are in 'absolute mode'. // Take this number of bytes from the stream as uncompressed data. int length = cmd[1]; + int length3 = length * 3; - byte[] run = new byte[length * 3]; + Span run = length3 <= 128 ? scratchBuffer.Slice(0, length3) : new byte[length3]; - stream.Read(run, 0, run.Length); + stream.Read(run); - run.AsSpan().CopyTo(buffer[(uncompressedPixels * 3)..]); + run.CopyTo(buffer[(uncompressedPixels * 3)..]); uncompressedPixels += length; // Absolute mode data is aligned to two-byte word-boundary. - int padding = run.Length & 1; + int padding = length3 & 1; stream.Skip(padding); @@ -1286,18 +1290,18 @@ private void ReadInfoHeader(BufferedReadStream stream) // color masks for each color channel follow the info header. if (this.infoHeader.Compression == BmpCompression.BitFields) { - byte[] bitfieldsBuffer = new byte[12]; - stream.Read(bitfieldsBuffer, 0, 12); - Span data = bitfieldsBuffer.AsSpan(); + Span bitfieldsBuffer = stackalloc byte[12]; + stream.Read(bitfieldsBuffer); + Span data = bitfieldsBuffer; this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); } else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS) { - byte[] bitfieldsBuffer = new byte[16]; - stream.Read(bitfieldsBuffer, 0, 16); - Span data = bitfieldsBuffer.AsSpan(); + Span bitfieldsBuffer = stackalloc byte[16]; + stream.Read(bitfieldsBuffer); + Span data = bitfieldsBuffer; this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); @@ -1470,7 +1474,7 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b { // Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit. // Make sure, that we will not read pass the bitmap offset (starting position of image data). - if ((stream.Position + colorMapSizeBytes) > this.fileHeader.Offset) + if (stream.Position > this.fileHeader.Offset - colorMapSizeBytes) { BmpThrowHelper.ThrowInvalidImageContentException( $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset."); From 5d65ef0afdef0fd59f41af60ecc07bfe0a57c696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 22:37:07 +0100 Subject: [PATCH 08/10] Reduced intermediate allocations: Profiles --- .../Metadata/Profiles/Exif/ExifReader.cs | 33 ++++++++++++------- .../Profiles/Exif/Values/ExifByteArray.cs | 4 +-- .../Metadata/Profiles/ICC/IccProfile.cs | 14 ++++---- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 885db3a5e9..953ef74afb 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -86,10 +86,6 @@ private void GetThumbnail(uint offset) /// internal abstract class BaseExifReader { - private readonly byte[] buf8 = new byte[8]; - private readonly byte[] buf4 = new byte[4]; - private readonly byte[] buf2 = new byte[2]; - private readonly MemoryAllocator? allocator; private readonly Stream data; private List? invalidTags; @@ -528,20 +524,33 @@ private bool TryReadSpan(Span span) return read == length; } - protected ulong ReadUInt64() => - this.TryReadSpan(this.buf8) - ? this.ConvertToUInt64(this.buf8) + protected ulong ReadUInt64() + { + Span buffer = stackalloc byte[8]; + + return this.TryReadSpan(buffer) + ? this.ConvertToUInt64(buffer) : default; + } // Known as Long in Exif Specification. - protected uint ReadUInt32() => - this.TryReadSpan(this.buf4) - ? this.ConvertToUInt32(this.buf4) + protected uint ReadUInt32() + { + Span buffer = stackalloc byte[4]; + + return this.TryReadSpan(buffer) + ? this.ConvertToUInt32(buffer) : default; + } - protected ushort ReadUInt16() => this.TryReadSpan(this.buf2) - ? this.ConvertToShort(this.buf2) + protected ushort ReadUInt16() + { + Span buffer = stackalloc byte[2]; + + return this.TryReadSpan(buffer) + ? this.ConvertToShort(buffer) : default; + } private long ConvertToInt64(ReadOnlySpan buffer) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs index 4320cb5e82..6811fc6f9c 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs @@ -45,12 +45,12 @@ public override bool TrySetValue(object? value) private bool TrySetSignedIntArray(int[] intArrayValue) { - if (Array.FindIndex(intArrayValue, x => x < byte.MinValue || x > byte.MaxValue) > -1) + if (Array.FindIndex(intArrayValue, x => (uint)x > byte.MaxValue) >= 0) { return false; } - var value = new byte[intArrayValue.Length]; + byte[] value = new byte[intArrayValue.Length]; for (int i = 0; i < intArrayValue.Length; i++) { int s = intArrayValue[i]; diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 5699f9bf36..3b5e438299 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -108,10 +108,10 @@ public static IccProfileId CalculateHash(byte[] data) const int profileIdPos = 84; // need to copy some values because they need to be zero for the hashing - byte[] temp = new byte[24]; - Buffer.BlockCopy(data, profileFlagPos, temp, 0, 4); - Buffer.BlockCopy(data, renderingIntentPos, temp, 4, 4); - Buffer.BlockCopy(data, profileIdPos, temp, 8, 16); + Span temp = stackalloc byte[24]; + data.AsSpan(profileFlagPos, 4).CopyTo(temp); + data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4)); + data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8)); try { @@ -131,9 +131,9 @@ public static IccProfileId CalculateHash(byte[] data) } finally { - Buffer.BlockCopy(temp, 0, data, profileFlagPos, 4); - Buffer.BlockCopy(temp, 4, data, renderingIntentPos, 4); - Buffer.BlockCopy(temp, 8, data, profileIdPos, 16); + temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos)); + temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos)); + temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos)); } } From 57d0793130a3a71dd896df6bd8e2696fefa9a190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sat, 25 Mar 2023 23:04:02 +0100 Subject: [PATCH 09/10] Fixed build warnings / errors --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 2 +- src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs | 6 +++--- src/ImageSharp/Formats/Webp/WebpDecoderCore.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 3c383e7766..83a828caaf 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -125,7 +125,7 @@ public JpegDecoderCore(JpegDecoderOptions options) } /// - /// The only supported precision + /// Gets the only supported precisions /// // Refers to assembly's static data segment, no allocation occurs. private static ReadOnlySpan SupportedPrecisions => new byte[] { 8, 12 }; diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index cb13825bc2..7952b15b44 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -361,6 +361,9 @@ private void ReconstructRow(Vp8Decoder dec) } } + Span scratch = stackalloc int[16]; + Span scratchBytes = stackalloc byte[4]; + // Reconstruct one row. for (int mbx = 0; mbx < dec.MbWidth; mbx++) { @@ -399,9 +402,6 @@ private void ReconstructRow(Vp8Decoder dec) topYuv.V.CopyTo(yuv[(vOff - WebpConstants.Bps)..]); } - Span scratch = stackalloc int[16]; - Span scratchBytes = stackalloc byte[4]; - // Predict and add residuals. if (block.IsI4x4) { diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 0d19dda023..223e15a0e7 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -228,7 +228,7 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad /// The chunk type. /// The webp image features. /// For identify, the alpha data should not be read. - /// Temporary buffer. + /// Temporary buffer. /// true, if its a alpha chunk. private bool ParseOptionalExtendedChunks( BufferedReadStream stream, From 7b2923d7efe82188f35fd86bb61fbd450160c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Sun, 26 Mar 2023 13:44:25 +0200 Subject: [PATCH 10/10] Use constant to specify the size of the buffer It's only one value for the fixed buffer size and the creation of the span -- so less error prone once the value needs to be updated. --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 5 +++-- .../Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs | 1 + src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +++-- src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs | 5 +++-- src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs | 5 +++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index efde4e9aa8..55ad2c4585 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -765,8 +765,9 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s private unsafe struct ScratchBuffer { - private fixed byte scratch[16]; + private const int Size = 16; + private fixed byte scratch[Size]; - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 16); + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index 5ecf779615..02a346ff07 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -53,6 +53,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder private ArithmeticDecodingTable[] acDecodingTables; + // Don't make this a ReadOnlySpan, as the values need to get updated. private readonly byte[] fixedBin = { 113, 0, 0, 0 }; private readonly CancellationToken cancellationToken; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a2edbc4c3f..fb1d33277a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1412,8 +1412,9 @@ private static PngBitDepth SuggestBitDepth() private unsafe struct ScratchBuffer { - private fixed byte scratch[16]; + private const int Size = 16; + private fixed byte scratch[Size]; - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 16); + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 8baf2cc156..ab78d18604 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -260,8 +260,9 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi private unsafe struct ScratchBuffer { - private fixed byte scratch[4]; + private const int Size = 4; + private fixed byte scratch[Size]; - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 4); + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index e3c2797bf3..1f7c7586eb 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -1834,8 +1834,9 @@ public void Dispose() /// private unsafe struct ScratchBuffer { - private fixed int scratch[256]; + private const int Size = 256; + private fixed int scratch[Size]; - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], 256); + public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); } }