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 @@ internal readonly struct AdobeMarker : IEquatable /// /// 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 @@ internal readonly struct JFifMarker : IEquatable /// /// 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } // 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 sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } else { - this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer); } break; @@ -755,8 +750,10 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// /// 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals } // 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 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals cancellationToken.ThrowIfCancellationRequested(); this.outputStream = stream; + Span buffer = stackalloc byte[20]; ImageMetadata metadata = image.Metadata; JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); @@ -76,39 +72,39 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals // - 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals // - 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals { 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// /// 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 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals /// 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;