Skip to content
38 changes: 21 additions & 17 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ private void ReadRle24<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixel
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle4(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{
Span<byte> scratchBuffer = stackalloc byte[128];
Span<byte> cmd = stackalloc byte[2];
int count = 0;

Expand Down Expand Up @@ -491,9 +492,9 @@ private void UncompressRle4(BufferedReadStream stream, int w, Span<byte> buffer,
int max = cmd[1];
int bytesToRead = (int)(((uint)max + 1) / 2);

byte[] run = new byte[bytesToRead];
Span<byte> 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++)
Expand Down Expand Up @@ -559,6 +560,7 @@ private void UncompressRle4(BufferedReadStream stream, int w, Span<byte> buffer,
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle8(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{
Span<byte> scratchBuffer = stackalloc byte[128];
Span<byte> cmd = stackalloc byte[2];
int count = 0;

Expand Down Expand Up @@ -596,13 +598,13 @@ private void UncompressRle8(BufferedReadStream stream, int w, Span<byte> buffer,
// Take this number of bytes from the stream as uncompressed data.
int length = cmd[1];

byte[] run = new byte[length];
Span<byte> 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;
Expand Down Expand Up @@ -639,6 +641,7 @@ private void UncompressRle8(BufferedReadStream stream, int w, Span<byte> buffer,
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle24(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{
Span<byte> scratchBuffer = stackalloc byte[128];
Span<byte> cmd = stackalloc byte[2];
int uncompressedPixels = 0;

Expand Down Expand Up @@ -675,17 +678,18 @@ private void UncompressRle24(BufferedReadStream stream, int w, Span<byte> 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<byte> 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);

Expand Down Expand Up @@ -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<byte> data = bitfieldsBuffer.AsSpan();
Span<byte> bitfieldsBuffer = stackalloc byte[12];
stream.Read(bitfieldsBuffer);
Span<byte> 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<byte> data = bitfieldsBuffer.AsSpan();
Span<byte> bitfieldsBuffer = stackalloc byte[16];
stream.Read(bitfieldsBuffer);
Span<byte> 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));
Expand Down Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just because I'm paranoid of overflow 😉.

{
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.");
Expand Down
30 changes: 19 additions & 11 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary>
/// The temp buffer used to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[16];
private ScratchBuffer buffer; // mutable struct, don't make readonly
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ScratchBuffer also has 16 bytes and is embedded into this class. It saves the additional object the GC needs to track + the overhead of object / array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we use scratch buffers in a few places.


/// <summary>
/// The global color table.
Expand Down Expand Up @@ -249,13 +249,13 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
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);
}

/// <summary>
Expand All @@ -264,13 +264,13 @@ private void ReadGraphicalControlExtension(BufferedReadStream stream)
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
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");
Expand All @@ -283,13 +283,13 @@ private void ReadImageDescriptor(BufferedReadStream stream)
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
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);
}

/// <summary>
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -762,4 +762,12 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
}
}
}

private unsafe struct ScratchBuffer
{
private const int Size = 16;
private fixed byte scratch[Size];

public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
}
32 changes: 16 additions & 16 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary>
private readonly Configuration configuration;

/// <summary>
/// A reusable buffer used to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[20];

/// <summary>
/// Whether to skip metadata during encode.
/// </summary>
Expand Down Expand Up @@ -324,9 +319,10 @@ private void WriteLogicalScreenDescriptor(
backgroundColorIndex: unchecked((byte)transparencyIndex),
ratio);

descriptor.WriteTo(this.buffer);
Span<byte> buffer = stackalloc byte[20];
descriptor.WriteTo(buffer);

stream.Write(this.buffer, 0, GifLogicalScreenDescriptor.Size);
stream.Write(buffer, 0, GifLogicalScreenDescriptor.Size);
}

/// <summary>
Expand Down Expand Up @@ -365,12 +361,14 @@ private void WriteComments(GifMetadata metadata, Stream stream)
return;
}

Span<byte> 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;
Comment on lines +369 to +370
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll write below a comment on why these are flipped.

stream.Write(buffer);

// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
Expand Down Expand Up @@ -437,22 +435,23 @@ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int trans
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer;
int extensionSize = extension.ContentLength;

if (extensionSize == 0)
{
return;
}
else if (extensionSize > this.buffer.Length - 3)

IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
if (extensionSize > 128)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
extensionBuffer = owner.GetSpan();
}
else
{
extensionBuffer = this.buffer;
extensionBuffer = stackalloc byte[extensionSize + 3];
}

extensionBuffer[0] = GifConstants.ExtensionIntroducer;
Expand Down Expand Up @@ -489,9 +488,10 @@ private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColo
height: (ushort)image.Height,
packed: packedValue);

descriptor.WriteTo(this.buffer);
Span<byte> buffer = stackalloc byte[20];
descriptor.WriteTo(buffer);

stream.Write(this.buffer, 0, GifImageDescriptor.Size);
stream.Write(buffer, 0, GifImageDescriptor.Size);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private AdobeMarker(short dctEncodeVersion, short app14Flags0, short app14Flags1
/// </summary>
/// <param name="bytes">The byte array containing metadata to parse.</param>
/// <param name="marker">The marker to return.</param>
public static bool TryParse(byte[] bytes, out AdobeMarker marker)
public static bool TryParse(ReadOnlySpan<byte> bytes, out AdobeMarker marker)
{
if (ProfileResolver.IsProfile(bytes, ProfileResolver.AdobeMarker))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder

private ArithmeticDecodingTable[] acDecodingTables;

// Don't make this a ReadOnlySpan<byte>, as the values need to get updated.
private readonly byte[] fixedBin = { 113, 0, 0, 0 };

private readonly CancellationToken cancellationToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private JFifMarker(byte majorVersion, byte minorVersion, byte densityUnits, shor
/// </summary>
/// <param name="bytes">The byte array containing metadata to parse.</param>
/// <param name="marker">The marker to return.</param>
public static bool TryParse(byte[] bytes, out JFifMarker marker)
public static bool TryParse(ReadOnlySpan<byte> bytes, out JFifMarker marker)
{
if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker))
{
Expand Down
Loading