Browse Source

Merge branch 'main' into fixed-improvements

pull/2418/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
df4ae9e6dc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 30
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  3. 32
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 122
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrArm.cs
  5. 133
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKArm64.cs
  6. 14
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
  7. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs
  8. 1
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
  9. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
  10. 146
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  11. 289
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  12. 57
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  13. 51
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  14. 43
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  15. 12
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  16. 11
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs
  17. 23
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs
  18. 30
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs
  19. 9
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs
  20. 38
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  21. 44
      src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs
  22. 2
      src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs
  23. 23
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  24. 15
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  25. 32
      src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
  26. 26
      src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
  27. 16
      src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
  28. 58
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  29. 5
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  30. 2
      src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs
  31. 8
      src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
  32. 20
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  33. 25
      src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs
  34. 11
      src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs
  35. 27
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  36. 35
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  37. 26
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  38. 104
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  39. 33
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  40. 4
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs
  41. 14
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  42. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs
  43. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs
  44. 237
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
  45. 4
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs
  46. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs
  47. 15
      tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs

38
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -453,6 +453,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <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) 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]; Span<byte> cmd = stackalloc byte[2];
int count = 0; int count = 0;
@ -491,9 +492,9 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
int max = cmd[1]; int max = cmd[1];
int bytesToRead = (int)(((uint)max + 1) / 2); 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; int idx = 0;
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
@ -559,6 +560,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <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) 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]; Span<byte> cmd = stackalloc byte[2];
int count = 0; int count = 0;
@ -596,13 +598,13 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Take this number of bytes from the stream as uncompressed data. // Take this number of bytes from the stream as uncompressed data.
int length = cmd[1]; 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. // Absolute mode data is aligned to two-byte word-boundary.
int padding = length & 1; int padding = length & 1;
@ -639,6 +641,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <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) 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]; Span<byte> cmd = stackalloc byte[2];
int uncompressedPixels = 0; int uncompressedPixels = 0;
@ -675,17 +678,18 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// If the second byte > 2, we are in 'absolute mode'. // If the second byte > 2, we are in 'absolute mode'.
// Take this number of bytes from the stream as uncompressed data. // Take this number of bytes from the stream as uncompressed data.
int length = cmd[1]; 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; uncompressedPixels += length;
// Absolute mode data is aligned to two-byte word-boundary. // Absolute mode data is aligned to two-byte word-boundary.
int padding = run.Length & 1; int padding = length3 & 1;
stream.Skip(padding); stream.Skip(padding);
@ -1286,18 +1290,18 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// color masks for each color channel follow the info header. // color masks for each color channel follow the info header.
if (this.infoHeader.Compression == BmpCompression.BitFields) if (this.infoHeader.Compression == BmpCompression.BitFields)
{ {
byte[] bitfieldsBuffer = new byte[12]; Span<byte> bitfieldsBuffer = stackalloc byte[12];
stream.Read(bitfieldsBuffer, 0, 12); stream.Read(bitfieldsBuffer);
Span<byte> data = bitfieldsBuffer.AsSpan(); Span<byte> data = bitfieldsBuffer;
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
} }
else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS) else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS)
{ {
byte[] bitfieldsBuffer = new byte[16]; Span<byte> bitfieldsBuffer = stackalloc byte[16];
stream.Read(bitfieldsBuffer, 0, 16); stream.Read(bitfieldsBuffer);
Span<byte> data = bitfieldsBuffer.AsSpan(); Span<byte> data = bitfieldsBuffer;
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)); this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
@ -1470,7 +1474,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
{ {
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit. // 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). // 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( 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."); $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");

30
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -22,7 +22,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The temp buffer used to reduce allocations. /// The temp buffer used to reduce allocations.
/// </summary> /// </summary>
private readonly byte[] buffer = new byte[16]; private ScratchBuffer buffer; // mutable struct, don't make readonly
/// <summary> /// <summary>
/// The global color table. /// The global color table.
@ -249,13 +249,13 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadGraphicalControlExtension(BufferedReadStream stream) 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) if (bytesRead != 6)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); 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> /// <summary>
@ -264,13 +264,13 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadImageDescriptor(BufferedReadStream stream) 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) if (bytesRead != 9)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); 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) if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
@ -283,13 +283,13 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadLogicalScreenDescriptor(BufferedReadStream stream) 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) if (bytesRead != 7)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); 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> /// <summary>
@ -306,8 +306,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
long position = stream.Position; long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize) if (appLength == GifConstants.ApplicationBlockSize)
{ {
stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp && !this.skipMetadata) if (isXmp && !this.skipMetadata)
{ {
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
@ -331,8 +331,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount;
stream.Skip(1); // Skip the terminator. stream.Skip(1); // Skip the terminator.
return; return;
} }
@ -762,4 +762,12 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
} }
} }
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
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -28,11 +28,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly Configuration configuration; private readonly Configuration configuration;
/// <summary>
/// A reusable buffer used to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[20];
/// <summary> /// <summary>
/// Whether to skip metadata during encode. /// Whether to skip metadata during encode.
/// </summary> /// </summary>
@ -324,9 +319,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
backgroundColorIndex: unchecked((byte)transparencyIndex), backgroundColorIndex: unchecked((byte)transparencyIndex),
ratio); 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> /// <summary>
@ -365,12 +361,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
return; return;
} }
Span<byte> buffer = stackalloc byte[2];
for (int i = 0; i < metadata.Comments.Count; i++) for (int i = 0; i < metadata.Comments.Count; i++)
{ {
string comment = metadata.Comments[i]; string comment = metadata.Comments[i];
this.buffer[0] = GifConstants.ExtensionIntroducer; buffer[1] = GifConstants.CommentLabel;
this.buffer[1] = GifConstants.CommentLabel; buffer[0] = GifConstants.ExtensionIntroducer;
stream.Write(this.buffer, 0, 2); stream.Write(buffer);
// Comment will be stored in chunks of 255 bytes, if it exceeds this size. // Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan(); ReadOnlySpan<char> commentSpan = comment.AsSpan();
@ -437,22 +435,23 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream) private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension where TGifExtension : struct, IGifExtension
{ {
IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer;
int extensionSize = extension.ContentLength; int extensionSize = extension.ContentLength;
if (extensionSize == 0) if (extensionSize == 0)
{ {
return; 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); owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
extensionBuffer = owner.GetSpan(); extensionBuffer = owner.GetSpan();
} }
else else
{ {
extensionBuffer = this.buffer; extensionBuffer = stackalloc byte[extensionSize + 3];
} }
extensionBuffer[0] = GifConstants.ExtensionIntroducer; extensionBuffer[0] = GifConstants.ExtensionIntroducer;
@ -489,9 +488,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
height: (ushort)image.Height, height: (ushort)image.Height,
packed: packedValue); 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> /// <summary>

122
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrArm.cs

@ -0,0 +1,122 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal abstract partial class JpegColorConverterBase
{
internal sealed class YCbCrArm : JpegColorConverterArm
{
public YCbCrArm(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector128<float> c0Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> c1Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> c2Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
// Used for the color conversion
var chromaOffset = Vector128.Create(-this.HalfValue);
var scale = Vector128.Create(1 / this.MaximumValue);
var rCrMult = Vector128.Create(YCbCrScalar.RCrMult);
var gCbMult = Vector128.Create(-YCbCrScalar.GCbMult);
var gCrMult = Vector128.Create(-YCbCrScalar.GCrMult);
var bCbMult = Vector128.Create(YCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nuint n = (uint)values.Component0.Length / (uint)Vector128<float>.Count;
for (nuint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
ref Vector128<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector128<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector128<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector128<float> y = c0;
Vector128<float> cb = AdvSimd.Add(c1, chromaOffset);
Vector128<float> cr = AdvSimd.Add(c2, chromaOffset);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector128<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector128<float> g = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector128<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = AdvSimd.Multiply(AdvSimd.RoundToNearest(r), scale);
g = AdvSimd.Multiply(AdvSimd.RoundToNearest(g), scale);
b = AdvSimd.Multiply(AdvSimd.RoundToNearest(b), scale);
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector128<float> destY =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> destCb =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> destCr =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector128<float> srcR =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector128<float> srcG =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector128<float> srcB =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(bLane));
// Used for the color conversion
var chromaOffset = Vector128.Create(this.HalfValue);
var f0299 = Vector128.Create(0.299f);
var f0587 = Vector128.Create(0.587f);
var f0114 = Vector128.Create(0.114f);
var fn0168736 = Vector128.Create(-0.168736f);
var fn0331264 = Vector128.Create(-0.331264f);
var fn0418688 = Vector128.Create(-0.418688f);
var fn0081312F = Vector128.Create(-0.081312F);
var f05 = Vector128.Create(0.5f);
nuint n = (uint)values.Component0.Length / (uint)Vector128<float>.Count;
for (nuint i = 0; i < n; i++)
{
Vector128<float> r = Unsafe.Add(ref srcR, i);
Vector128<float> g = Unsafe.Add(ref srcG, i);
Vector128<float> b = Unsafe.Add(ref srcB, i);
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Vector128<float> y = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(f0114, b), f0587, g), f0299, r);
Vector128<float> cb = AdvSimd.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(f05, b), fn0331264, g), fn0168736, r));
Vector128<float> cr = AdvSimd.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(fn0081312F, b), fn0418688, g), f05, r));
Unsafe.Add(ref destY, i) = y;
Unsafe.Add(ref destCb, i) = cb;
Unsafe.Add(ref destCr, i) = cr;
}
}
}
}

133
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKArm64.cs

@ -0,0 +1,133 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal abstract partial class JpegColorConverterBase
{
internal sealed class YccKArm64 : JpegColorConverterArm64
{
public YccKArm64(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector128<float> c0Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> c1Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> c2Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector128<float> kBase =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var chromaOffset = Vector128.Create(-this.HalfValue);
var scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue));
var max = Vector128.Create(this.MaximumValue);
var rCrMult = Vector128.Create(YCbCrScalar.RCrMult);
var gCbMult = Vector128.Create(-YCbCrScalar.GCbMult);
var gCrMult = Vector128.Create(-YCbCrScalar.GCrMult);
var bCbMult = Vector128.Create(YCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nuint n = (uint)values.Component0.Length / (uint)Vector128<float>.Count;
for (nuint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
// k = kVals[i] / 256F;
ref Vector128<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector128<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector128<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector128<float> y = c0;
Vector128<float> cb = AdvSimd.Add(c1, chromaOffset);
Vector128<float> cr = AdvSimd.Add(c2, chromaOffset);
Vector128<float> scaledK = AdvSimd.Multiply(Unsafe.Add(ref kBase, i), scale);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector128<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector128<float> g =
HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector128<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = AdvSimd.Subtract(max, AdvSimd.RoundToNearest(r));
g = AdvSimd.Subtract(max, AdvSimd.RoundToNearest(g));
b = AdvSimd.Subtract(max, AdvSimd.RoundToNearest(b));
r = AdvSimd.Multiply(r, scaledK);
g = AdvSimd.Multiply(g, scaledK);
b = AdvSimd.Multiply(b, scaledK);
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
// rgb -> cmyk
CmykArm64.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane);
// cmyk -> ycck
ref Vector128<float> destY =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> destCb =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> destCr =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector128<float> srcR = ref destY;
ref Vector128<float> srcG = ref destCb;
ref Vector128<float> srcB = ref destCr;
// Used for the color conversion
var maxSampleValue = Vector128.Create(this.MaximumValue);
var chromaOffset = Vector128.Create(this.HalfValue);
var f0299 = Vector128.Create(0.299f);
var f0587 = Vector128.Create(0.587f);
var f0114 = Vector128.Create(0.114f);
var fn0168736 = Vector128.Create(-0.168736f);
var fn0331264 = Vector128.Create(-0.331264f);
var fn0418688 = Vector128.Create(-0.418688f);
var fn0081312F = Vector128.Create(-0.081312F);
var f05 = Vector128.Create(0.5f);
nuint n = (uint)values.Component0.Length / (uint)Vector128<float>.Count;
for (nuint i = 0; i < n; i++)
{
Vector128<float> r = AdvSimd.Subtract(maxSampleValue, Unsafe.Add(ref srcR, i));
Vector128<float> g = AdvSimd.Subtract(maxSampleValue, Unsafe.Add(ref srcG, i));
Vector128<float> b = AdvSimd.Subtract(maxSampleValue, Unsafe.Add(ref srcB, i));
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Vector128<float> y = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(f0114, b), f0587, g), f0299, r);
Vector128<float> cb = AdvSimd.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(f05, b), fn0331264, g), fn0168736, r));
Vector128<float> cr = AdvSimd.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(AdvSimd.Multiply(fn0081312F, b), fn0418688, g), f05, r));
Unsafe.Add(ref destY, i) = y;
Unsafe.Add(ref destCb, i) = cb;
Unsafe.Add(ref destCr, i) = cr;
}
}
}
}

14
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs

@ -138,6 +138,11 @@ internal abstract partial class JpegColorConverterBase
return new YCbCrAvx(precision); return new YCbCrAvx(precision);
} }
if (JpegColorConverterArm.IsSupported)
{
return new YCbCrArm(precision);
}
if (JpegColorConverterVector.IsSupported) if (JpegColorConverterVector.IsSupported)
{ {
return new YCbCrVector(precision); return new YCbCrVector(precision);
@ -157,6 +162,11 @@ internal abstract partial class JpegColorConverterBase
return new YccKAvx(precision); return new YccKAvx(precision);
} }
if (JpegColorConverterArm64.IsSupported)
{
return new YccKArm64(precision);
}
if (JpegColorConverterVector.IsSupported) if (JpegColorConverterVector.IsSupported)
{ {
return new YccKVector(precision); return new YccKVector(precision);
@ -231,10 +241,10 @@ internal abstract partial class JpegColorConverterBase
if (JpegColorConverterVector.IsSupported) if (JpegColorConverterVector.IsSupported)
{ {
return new RgbScalar(precision); return new RgbVector(precision);
} }
return new GrayscaleScalar(precision); return new RgbScalar(precision);
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs

@ -62,7 +62,7 @@ internal readonly struct AdobeMarker : IEquatable<AdobeMarker>
/// </summary> /// </summary>
/// <param name="bytes">The byte array containing metadata to parse.</param> /// <param name="bytes">The byte array containing metadata to parse.</param>
/// <param name="marker">The marker to return.</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)) if (ProfileResolver.IsProfile(bytes, ProfileResolver.AdobeMarker))
{ {

1
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs

@ -53,6 +53,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder
private ArithmeticDecodingTable[] acDecodingTables; 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 byte[] fixedBin = { 113, 0, 0, 0 };
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs

@ -69,7 +69,7 @@ internal readonly struct JFifMarker : IEquatable<JFifMarker>
/// </summary> /// </summary>
/// <param name="bytes">The byte array containing metadata to parse.</param> /// <param name="bytes">The byte array containing metadata to parse.</param>
/// <param name="marker">The marker to return.</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)) if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker))
{ {

146
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -27,21 +27,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg;
/// </summary> /// </summary>
internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
{ {
/// <summary>
/// The only supported precision
/// </summary>
private readonly byte[] supportedPrecisions = { 8, 12 };
/// <summary>
/// The buffer used to temporarily store bytes read from the stream.
/// </summary>
private readonly byte[] temp = new byte[2 * 16 * 4];
/// <summary>
/// The buffer used to read markers from the stream.
/// </summary>
private readonly byte[] markerBuffer = new byte[2];
/// <summary> /// <summary>
/// Whether the image has an EXIF marker. /// Whether the image has an EXIF marker.
/// </summary> /// </summary>
@ -139,6 +124,12 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
} }
/// <summary>
/// Gets the only supported precisions
/// </summary>
// Refers to assembly's static data segment, no allocation occurs.
private static ReadOnlySpan<byte> SupportedPrecisions => new byte[] { 8, 12 };
/// <inheritdoc /> /// <inheritdoc />
public DecoderOptions Options { get; } public DecoderOptions Options { get; }
@ -257,24 +248,26 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
using MemoryStream ms = new(tableBytes); using MemoryStream ms = new(tableBytes);
using BufferedReadStream stream = new(this.configuration, ms); using BufferedReadStream stream = new(this.configuration, ms);
Span<byte> markerBuffer = stackalloc byte[2];
// Check for the Start Of Image marker. // Check for the Start Of Image marker.
int bytesRead = stream.Read(this.markerBuffer, 0, 2); int bytesRead = stream.Read(markerBuffer);
JpegFileMarker fileMarker = new(this.markerBuffer[1], 0); JpegFileMarker fileMarker = new(markerBuffer[1], 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI) if (fileMarker.Marker != JpegConstants.Markers.SOI)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker.");
} }
// Read next marker. // Read next marker.
bytesRead = stream.Read(this.markerBuffer, 0, 2); bytesRead = stream.Read(markerBuffer);
fileMarker = new JpegFileMarker(this.markerBuffer[1], (int)stream.Position - 2); fileMarker = new JpegFileMarker(markerBuffer[1], (int)stream.Position - 2);
while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid))
{ {
if (!fileMarker.Invalid) if (!fileMarker.Invalid)
{ {
// Get the marker length. // 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 // Check whether the stream actually has enough bytes to read
// markerContentByteSize is always positive so we cast // markerContentByteSize is always positive so we cast
@ -297,7 +290,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.DRI: case JpegConstants.Markers.DRI:
this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer);
break; break;
case JpegConstants.Markers.EOI: case JpegConstants.Markers.EOI:
return; return;
@ -305,13 +298,13 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
// Read next marker. // Read next marker.
bytesRead = stream.Read(this.markerBuffer, 0, 2); bytesRead = stream.Read(markerBuffer);
if (bytesRead != 2) if (bytesRead != 2)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); 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(); this.Metadata = new ImageMetadata();
Span<byte> markerBuffer = stackalloc byte[2];
// Check for the Start Of Image marker. // Check for the Start Of Image marker.
stream.Read(this.markerBuffer, 0, 2); stream.Read(markerBuffer);
JpegFileMarker fileMarker = new(this.markerBuffer[1], 0); JpegFileMarker fileMarker = new(markerBuffer[1], 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI) if (fileMarker.Marker != JpegConstants.Markers.SOI)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker.");
@ -349,7 +344,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
if (!fileMarker.Invalid) if (!fileMarker.Invalid)
{ {
// Get the marker length. // 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 // Check whether stream actually has enough bytes to read
// markerContentByteSize is always positive so we cast // markerContentByteSize is always positive so we cast
@ -446,7 +441,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
else else
{ {
this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize, markerBuffer);
} }
break; break;
@ -755,8 +750,10 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
return; return;
} }
stream.Read(this.temp, 0, JFifMarker.Length); Span<byte> temp = stackalloc byte[2 * 16 * 4];
if (!JFifMarker.TryParse(this.temp, out this.jFif))
stream.Read(temp, 0, JFifMarker.Length);
if (!JFifMarker.TryParse(temp, out this.jFif))
{ {
JpegThrowHelper.ThrowNotSupportedException("Unknown App0 Marker - Expected JFIF."); JpegThrowHelper.ThrowNotSupportedException("Unknown App0 Marker - Expected JFIF.");
} }
@ -796,11 +793,13 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length.");
} }
Span<byte> temp = stackalloc byte[2 * 16 * 4];
// XMP marker is the longer then the EXIF marker, so first try read the EXIF marker bytes. // 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; remaining -= exifMarkerLength;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) if (ProfileResolver.IsProfile(temp, ProfileResolver.ExifMarker))
{ {
this.hasExif = true; this.hasExif = true;
byte[] profile = new byte[remaining]; byte[] profile = new byte[remaining];
@ -819,7 +818,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
remaining = 0; remaining = 0;
} }
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker[..exifMarkerLength])) if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength]))
{ {
const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength; const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength;
if (remaining < remainingXmpMarkerBytes || this.skipMetadata) if (remaining < remainingXmpMarkerBytes || this.skipMetadata)
@ -829,9 +828,9 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
return; return;
} }
stream.Read(this.temp, exifMarkerLength, remainingXmpMarkerBytes); stream.Read(temp, exifMarkerLength, remainingXmpMarkerBytes);
remaining -= remainingXmpMarkerBytes; remaining -= remainingXmpMarkerBytes;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker)) if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker))
{ {
this.hasXmp = true; this.hasXmp = true;
byte[] profile = new byte[remaining]; byte[] profile = new byte[remaining];
@ -870,8 +869,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
return; return;
} }
byte[] identifier = new byte[icclength]; Span<byte> identifier = stackalloc byte[icclength];
stream.Read(identifier, 0, icclength); stream.Read(identifier);
remaining -= icclength; // We have read it by this point remaining -= icclength; // We have read it by this point
if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker))
@ -911,13 +910,13 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
return; return;
} }
stream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length); Span<byte> temp = stackalloc byte[2 * 16 * 4];
stream.Read(temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length);
remaining -= 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]; Span<byte> blockDataSpan = remaining <= 128 ? stackalloc byte[remaining] : new byte[remaining];
stream.Read(resourceBlockData, 0, remaining); stream.Read(blockDataSpan);
Span<byte> blockDataSpan = resourceBlockData.AsSpan();
while (blockDataSpan.Length > 12) while (blockDataSpan.Length > 12)
{ {
@ -1047,10 +1046,12 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
return; return;
} }
stream.Read(this.temp, 0, markerLength); Span<byte> temp = stackalloc byte[2 * 16 * 4];
stream.Read(temp, 0, markerLength);
remaining -= markerLength; remaining -= markerLength;
if (AdobeMarker.TryParse(this.temp, out this.adobe)) if (AdobeMarker.TryParse(temp, out this.adobe))
{ {
this.hasAdobeMarker = true; this.hasAdobeMarker = true;
} }
@ -1072,6 +1073,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining)
{ {
JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);
Span<byte> temp = stackalloc byte[2 * 16 * 4];
while (remaining > 0) while (remaining > 0)
{ {
@ -1102,13 +1104,13 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining);
} }
stream.Read(this.temp, 0, 64); stream.Read(temp, 0, 64);
remaining -= 64; remaining -= 64;
// Parsing quantization table & saving it in natural order // Parsing quantization table & saving it in natural order
for (int j = 0; j < 64; j++) for (int j = 0; j < 64; j++)
{ {
table[ZigZag.ZigZagOrder[j]] = this.temp[j]; table[ZigZag.ZigZagOrder[j]] = temp[j];
} }
break; break;
@ -1121,13 +1123,13 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining);
} }
stream.Read(this.temp, 0, 128); stream.Read(temp, 0, 128);
remaining -= 128; remaining -= 128;
// Parsing quantization table & saving it in natural order // Parsing quantization table & saving it in natural order
for (int j = 0; j < 64; j++) 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; break;
@ -1174,28 +1176,30 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported.");
} }
Span<byte> temp = stackalloc byte[2 * 16 * 4];
// Read initial marker definitions. // Read initial marker definitions.
const int length = 6; const int length = 6;
int bytesRead = stream.Read(this.temp, 0, length); int bytesRead = stream.Read(temp, 0, length);
if (bytesRead != length) if (bytesRead != length)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data."); JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data.");
} }
// 1 byte: Bits/sample precision. // 1 byte: Bits/sample precision.
byte precision = this.temp[0]; byte precision = temp[0];
// Validate: only 8-bit and 12-bit precisions are supported. // 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."); JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision is supported.");
} }
// 2 byte: Height // 2 byte: Height
int frameHeight = (this.temp[1] << 8) | this.temp[2]; int frameHeight = (temp[1] << 8) | temp[2];
// 2 byte: Width // 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). // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that).
if (frameHeight == 0 || frameWidth == 0) if (frameHeight == 0 || frameWidth == 0)
@ -1204,7 +1208,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
// 1 byte: Number of components. // 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 // Validate: componentCount more than 4 can lead to a buffer overflow during stream
// reading so we must limit it to 4. // reading so we must limit it to 4.
@ -1227,7 +1231,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
// components*3 bytes: component data // 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 // No need to pool this. They max out at 4
this.Frame.ComponentIds = new byte[componentCount]; 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++) for (int i = 0; i < this.Frame.Components.Length; i++)
{ {
// 1 byte: component identifier // 1 byte: component identifier
byte componentId = this.temp[index]; byte componentId = temp[index];
// 1 byte: component sampling factors // 1 byte: component sampling factors
byte hv = this.temp[index + 1]; byte hv = temp[index + 1];
int h = (hv >> 4) & 15; int h = (hv >> 4) & 15;
int v = hv & 15; int v = hv & 15;
@ -1270,7 +1274,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
// 1 byte: quantization table destination selector // 1 byte: quantization table destination selector
byte quantTableIndex = this.temp[index + 2]; byte quantTableIndex = temp[index + 2];
// Validate: 0-3 range // Validate: 0-3 range
if (quantTableIndex > 3) if (quantTableIndex > 3)
@ -1379,7 +1383,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining) /// <param name="markerBuffer">Scratch buffer.</param>
private void ProcessDefineRestartIntervalMarker(BufferedReadStream stream, int remaining, Span<byte> markerBuffer)
{ {
if (remaining != 2) 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. // 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. // 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) if (this.scanDecoder != null)
{ {
@ -1425,14 +1430,16 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining); JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining);
} }
Span<byte> temp = stackalloc byte[2 * 16 * 4];
// selectorsCount*2 bytes: component index + huffman tables indices // 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; this.Frame.Interleaved = this.Frame.ComponentCount == selectorsCount;
for (int i = 0; i < selectorsBytes; i += 2) for (int i = 0; i < selectorsBytes; i += 2)
{ {
// 1 byte: Component id // 1 byte: Component id
int componentSelectorId = this.temp[i]; int componentSelectorId = temp[i];
int componentIndex = -1; int componentIndex = -1;
for (int j = 0; j < this.Frame.ComponentIds.Length; j++) 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. // 1 byte: Huffman table selectors.
// 4 bits - dc // 4 bits - dc
// 4 bits - ac // 4 bits - ac
int tableSpec = this.temp[i + 1]; int tableSpec = temp[i + 1];
int dcTableIndex = tableSpec >> 4; int dcTableIndex = tableSpec >> 4;
int acTableIndex = tableSpec & 15; int acTableIndex = tableSpec & 15;
@ -1475,17 +1482,17 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
} }
// 3 bytes: Progressive scan decoding data. // 3 bytes: Progressive scan decoding data.
int bytesRead = stream.Read(this.temp, 0, 3); int bytesRead = stream.Read(temp, 0, 3);
if (bytesRead != 3) if (bytesRead != 3)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data"); 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.SuccessiveHigh = successiveApproximation >> 4;
this.scanDecoder.SuccessiveLow = successiveApproximation & 15; this.scanDecoder.SuccessiveLow = successiveApproximation & 15;
@ -1501,16 +1508,17 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes. /// Reads a <see cref="ushort"/> from the stream advancing it by two bytes.
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <param name="markerBuffer">The scratch buffer used for reading from the stream.</param>
/// <returns>The <see cref="ushort"/></returns> /// <returns>The <see cref="ushort"/></returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private ushort ReadUint16(BufferedReadStream stream) private static ushort ReadUint16(BufferedReadStream stream, Span<byte> markerBuffer)
{ {
int bytesRead = stream.Read(this.markerBuffer, 0, 2); int bytesRead = stream.Read(markerBuffer, 0, 2);
if (bytesRead != 2) if (bytesRead != 2)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort."); JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort.");
} }
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); return BinaryPrimitives.ReadUInt16BigEndian(markerBuffer);
} }
} }

289
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -25,11 +25,6 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs(); private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs();
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[20];
private readonly JpegEncoder encoder; private readonly JpegEncoder encoder;
/// <summary> /// <summary>
@ -67,6 +62,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
this.outputStream = stream; this.outputStream = stream;
Span<byte> buffer = stackalloc byte[20];
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
@ -76,39 +72,39 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
using JpegFrame frame = new(image, frameConfig, interleaved); using JpegFrame frame = new(image, frameConfig, interleaved);
// Write the Start Of Image marker. // Write the Start Of Image marker.
this.WriteStartOfImage(); this.WriteStartOfImage(buffer);
// Write APP0 marker // Write APP0 marker
if (frameConfig.AdobeColorTransformMarkerFlag is null) if (frameConfig.AdobeColorTransformMarkerFlag is null)
{ {
this.WriteJfifApplicationHeader(metadata); this.WriteJfifApplicationHeader(metadata, buffer);
} }
// Write APP14 marker with adobe color extension // Write APP14 marker with adobe color extension
else else
{ {
this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value); this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value, buffer);
} }
// Write Exif, XMP, ICC and IPTC profiles // Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata); this.WriteProfiles(metadata, buffer);
// Write the image dimensions. // Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, frameConfig); this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables. // Write the Huffman tables.
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables. // 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 // Write scans with actual pixel data
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables); using SpectralConverter<TPixel> 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. // Write the End Of Image marker.
this.WriteEndOfImageMarker(); this.WriteEndOfImageMarker(buffer);
stream.Flush(); stream.Flush();
} }
@ -116,58 +112,59 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Write the start of image marker. /// Write the start of image marker.
/// </summary> /// </summary>
private void WriteStartOfImage() private void WriteStartOfImage(Span<byte> buffer)
{ {
// Markers are always prefixed with 0xff. // Markers are always prefixed with 0xff.
this.buffer[0] = JpegConstants.Markers.XFF; buffer[1] = JpegConstants.Markers.SOI;
this.buffer[1] = JpegConstants.Markers.SOI; buffer[0] = JpegConstants.Markers.XFF;
this.outputStream.Write(this.buffer, 0, 2); this.outputStream.Write(buffer, 0, 2);
} }
/// <summary> /// <summary>
/// Writes the application header containing the JFIF identifier plus extra data. /// Writes the application header containing the JFIF identifier plus extra data.
/// </summary> /// </summary>
/// <param name="meta">The image metadata.</param> /// <param name="meta">The image metadata.</param>
private void WriteJfifApplicationHeader(ImageMetadata meta) /// <param name="buffer">Temporary buffer.</param>
private void WriteJfifApplicationHeader(ImageMetadata meta, Span<byte> buffer)
{ {
// Write the JFIF headers // Write the JFIF headers (highest index first to avoid additional bound checks)
this.buffer[0] = JpegConstants.Markers.XFF; buffer[10] = 0x01; // versionlo
this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker buffer[0] = JpegConstants.Markers.XFF;
this.buffer[2] = 0x00; buffer[1] = JpegConstants.Markers.APP0; // Application Marker
this.buffer[3] = 0x10; buffer[2] = 0x00;
this.buffer[4] = 0x4a; // J buffer[3] = 0x10;
this.buffer[5] = 0x46; // F buffer[4] = 0x4a; // J
this.buffer[6] = 0x49; // I buffer[5] = 0x46; // F
this.buffer[7] = 0x46; // F buffer[6] = 0x49; // I
this.buffer[8] = 0x00; // = "JFIF",'\0' buffer[7] = 0x46; // F
this.buffer[9] = 0x01; // versionhi buffer[8] = 0x00; // = "JFIF",'\0'
this.buffer[10] = 0x01; // versionlo buffer[9] = 0x01; // versionhi
// Resolution. Big Endian // Resolution. Big Endian
Span<byte> hResolution = this.buffer.AsSpan(12, 2); Span<byte> hResolution = buffer.Slice(12, 2);
Span<byte> vResolution = this.buffer.AsSpan(14, 2); Span<byte> vResolution = buffer.Slice(14, 2);
if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter)
{ {
// Scale down to PPI // 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(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution)));
} }
else else
{ {
// We can simply pass the value. // 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(hResolution, (short)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution));
} }
// No thumbnail // No thumbnail
this.buffer[16] = 0x00; // Thumbnail width buffer[17] = 0x00; // Thumbnail height
this.buffer[17] = 0x00; // Thumbnail height buffer[16] = 0x00; // Thumbnail width
this.outputStream.Write(this.buffer, 0, 18); this.outputStream.Write(buffer, 0, 18);
} }
/// <summary> /// <summary>
@ -175,8 +172,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <param name="tableConfigs">The table configuration.</param> /// <param name="tableConfigs">The table configuration.</param>
/// <param name="scanEncoder">The scan encoder.</param> /// <param name="scanEncoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ArgumentNullException"><paramref name="tableConfigs"/> is <see langword="null"/>.</exception> /// <exception cref="ArgumentNullException"><paramref name="tableConfigs"/> is <see langword="null"/>.</exception>
private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder) private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder, Span<byte> buffer)
{ {
if (tableConfigs is null) if (tableConfigs is null)
{ {
@ -190,7 +188,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length; 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++) for (int i = 0; i < tableConfigs.Length; i++)
{ {
JpegHuffmanTableConfig tableConfig = tableConfigs[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. /// Writes the APP14 marker to indicate the image is in RGB color space.
/// </summary> /// </summary>
/// <param name="colorTransform">The color transform byte.</param> /// <param name="colorTransform">The color transform byte.</param>
private void WriteApp14Marker(byte colorTransform) /// <param name="buffer">Temporary buffer.</param>
private void WriteApp14Marker(byte colorTransform, Span<byte> 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". // Identifier: ASCII "Adobe" (highest index first to avoid additional bound checks).
this.buffer[0] = 0x41; buffer[4] = 0x65;
this.buffer[1] = 0x64; buffer[0] = 0x41;
this.buffer[2] = 0x6F; buffer[1] = 0x64;
this.buffer[3] = 0x62; buffer[2] = 0x6F;
this.buffer[4] = 0x65; buffer[3] = 0x62;
// Version, currently 100. // Version, currently 100.
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(5, 2), 100); BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(5, 2), 100);
// Flags0 // Flags0
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(7, 2), 0); BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(7, 2), 0);
// Flags1 // Flags1
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0); BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(9, 2), 0);
// Color transform byte // 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));
} }
/// <summary> /// <summary>
/// Writes the EXIF profile. /// Writes the EXIF profile.
/// </summary> /// </summary>
/// <param name="exifProfile">The exif profile.</param> /// <param name="exifProfile">The exif profile.</param>
private void WriteExifProfile(ExifProfile exifProfile) /// <param name="buffer">Temporary buffer.</param>
private void WriteExifProfile(ExifProfile exifProfile, Span<byte> buffer)
{ {
if (exifProfile is null || exifProfile.Values.Count == 0) if (exifProfile is null || exifProfile.Values.Count == 0)
{ {
@ -262,7 +262,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
int app1Length = bytesToWrite + 2; int app1Length = bytesToWrite + 2;
// Write the app marker, EXIF marker, and data // 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(Components.Decoder.ProfileResolver.ExifMarker);
this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength); this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength);
remaining -= bytesToWrite; remaining -= bytesToWrite;
@ -273,7 +273,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining; bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining;
app1Length = bytesToWrite + 2 + exifMarkerLength; app1Length = bytesToWrite + 2 + exifMarkerLength;
this.WriteApp1Header(app1Length); this.WriteApp1Header(app1Length, buffer);
// Write Exif00 marker // Write Exif00 marker
this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker);
@ -289,10 +289,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// Writes the IPTC metadata. /// Writes the IPTC metadata.
/// </summary> /// </summary>
/// <param name="iptcProfile">The iptc metadata to write.</param> /// <param name="iptcProfile">The iptc metadata to write.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the IPTC profile size exceeds the limit of 65533 bytes. /// Thrown if the IPTC profile size exceeds the limit of 65533 bytes.
/// </exception> /// </exception>
private void WriteIptcProfile(IptcProfile iptcProfile) private void WriteIptcProfile(IptcProfile iptcProfile, Span<byte> buffer)
{ {
const int maxBytes = 65533; const int maxBytes = 65533;
if (iptcProfile is null || !iptcProfile.Values.Any()) 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.AdobeImageResourceBlockMarker.Length +
Components.Decoder.ProfileResolver.AdobeIptcMarker.Length + Components.Decoder.ProfileResolver.AdobeIptcMarker.Length +
2 + 4 + data.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.AdobePhotoshopApp13Marker);
this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker);
this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker);
this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even) this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even)
this.outputStream.WriteByte(0); this.outputStream.WriteByte(0);
BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length); BinaryPrimitives.WriteInt32BigEndian(buffer, data.Length);
this.outputStream.Write(this.buffer, 0, 4); this.outputStream.Write(buffer, 0, 4);
this.outputStream.Write(data, 0, data.Length); this.outputStream.Write(data, 0, data.Length);
} }
@ -331,10 +332,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// Writes the XMP metadata. /// Writes the XMP metadata.
/// </summary> /// </summary>
/// <param name="xmpProfile">The XMP metadata to write.</param> /// <param name="xmpProfile">The XMP metadata to write.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the XMP profile size exceeds the limit of 65533 bytes. /// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
/// </exception> /// </exception>
private void WriteXmpProfile(XmpProfile xmpProfile) private void WriteXmpProfile(XmpProfile xmpProfile, Span<byte> buffer)
{ {
if (xmpProfile is null) if (xmpProfile is null)
{ {
@ -367,7 +369,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
dataLength -= length; dataLength -= length;
int app1Length = 2 + Components.Decoder.ProfileResolver.XmpMarker.Length + 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(Components.Decoder.ProfileResolver.XmpMarker);
this.outputStream.Write(data, offset, length); this.outputStream.Write(data, offset, length);
@ -379,32 +381,35 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// Writes the App1 header. /// Writes the App1 header.
/// </summary> /// </summary>
/// <param name="app1Length">The length of the data the app1 marker contains.</param> /// <param name="app1Length">The length of the data the app1 marker contains.</param>
private void WriteApp1Header(int app1Length) /// <param name="buffer">Temporary buffer.</param>
=> this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1); private void WriteApp1Header(int app1Length, Span<byte> buffer)
=> this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1, buffer);
/// <summary> /// <summary>
/// Writes a AppX header. /// Writes a AppX header.
/// </summary> /// </summary>
/// <param name="length">The length of the data the app marker contains.</param> /// <param name="length">The length of the data the app marker contains.</param>
/// <param name="appMarker">The app marker to write.</param> /// <param name="appMarker">The app marker to write.</param>
private void WriteAppHeader(int length, byte appMarker) /// <param name="buffer">Temporary buffer.</param>
private void WriteAppHeader(int length, byte appMarker, Span<byte> buffer)
{ {
this.buffer[0] = JpegConstants.Markers.XFF; buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = appMarker; buffer[1] = appMarker;
this.buffer[2] = (byte)((length >> 8) & 0xFF); buffer[2] = (byte)((length >> 8) & 0xFF);
this.buffer[3] = (byte)(length & 0xFF); buffer[3] = (byte)(length & 0xFF);
this.outputStream.Write(this.buffer, 0, 4); this.outputStream.Write(buffer, 0, 4);
} }
/// <summary> /// <summary>
/// Writes the ICC profile. /// Writes the ICC profile.
/// </summary> /// </summary>
/// <param name="iccProfile">The ICC profile to write.</param> /// <param name="iccProfile">The ICC profile to write.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if any of the ICC profiles size exceeds the limit. /// Thrown if any of the ICC profiles size exceeds the limit.
/// </exception> /// </exception>
private void WriteIccProfile(IccProfile iccProfile) private void WriteIccProfile(IccProfile iccProfile, Span<byte> buffer)
{ {
if (iccProfile is null) if (iccProfile is null)
{ {
@ -446,30 +451,31 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
dataLength -= length; dataLength -= length;
this.buffer[0] = JpegConstants.Markers.XFF; buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.APP2; // Application Marker buffer[1] = JpegConstants.Markers.APP2; // Application Marker
int markerLength = length + 16; int markerLength = length + 16;
this.buffer[2] = (byte)((markerLength >> 8) & 0xFF); buffer[2] = (byte)((markerLength >> 8) & 0xFF);
this.buffer[3] = (byte)(markerLength & 0xFF); buffer[3] = (byte)(markerLength & 0xFF);
this.outputStream.Write(this.buffer, 0, 4); this.outputStream.Write(buffer, 0, 4);
this.buffer[0] = (byte)'I'; // We write the highest index first, to have only one bound check.
this.buffer[1] = (byte)'C'; buffer[13] = (byte)count; // The total number of profiles.
this.buffer[2] = (byte)'C'; buffer[12] = (byte)current; // The position within the collection.
this.buffer[3] = (byte)'_'; buffer[11] = 0x00;
this.buffer[4] = (byte)'P'; buffer[0] = (byte)'I';
this.buffer[5] = (byte)'R'; buffer[1] = (byte)'C';
this.buffer[6] = (byte)'O'; buffer[2] = (byte)'C';
this.buffer[7] = (byte)'F'; buffer[3] = (byte)'_';
this.buffer[8] = (byte)'I'; buffer[4] = (byte)'P';
this.buffer[9] = (byte)'L'; buffer[5] = (byte)'R';
this.buffer[10] = (byte)'E'; buffer[6] = (byte)'O';
this.buffer[11] = 0x00; buffer[7] = (byte)'F';
this.buffer[12] = (byte)current; // The position within the collection. buffer[8] = (byte)'I';
this.buffer[13] = (byte)count; // The total number of profiles. buffer[9] = (byte)'L';
buffer[10] = (byte)'E';
this.outputStream.Write(this.buffer, 0, iccOverheadLength);
this.outputStream.Write(buffer, 0, iccOverheadLength);
this.outputStream.Write(data, offset, length); this.outputStream.Write(data, offset, length);
current++; current++;
@ -481,7 +487,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// Writes the metadata profiles to the image. /// Writes the metadata profiles to the image.
/// </summary> /// </summary>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
private void WriteProfiles(ImageMetadata metadata) /// <param name="buffer">Temporary buffer.</param>
private void WriteProfiles(ImageMetadata metadata, Span<byte> buffer)
{ {
if (metadata is null) if (metadata is null)
{ {
@ -494,10 +501,10 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
// - APP2 ICC // - APP2 ICC
// - APP13 IPTC // - APP13 IPTC
metadata.SyncProfiles(); metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile); this.WriteExifProfile(metadata.ExifProfile, buffer);
this.WriteXmpProfile(metadata.XmpProfile); this.WriteXmpProfile(metadata.XmpProfile, buffer);
this.WriteIccProfile(metadata.IccProfile); this.WriteIccProfile(metadata.IccProfile, buffer);
this.WriteIptcProfile(metadata.IptcProfile); this.WriteIptcProfile(metadata.IptcProfile, buffer);
} }
/// <summary> /// <summary>
@ -506,25 +513,26 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <param name="width">The frame width.</param> /// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param> /// <param name="height">The frame height.</param>
/// <param name="frame">The frame configuration.</param> /// <param name="frame">The frame configuration.</param>
private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) /// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Span<byte> buffer)
{ {
JpegComponentConfig[] components = frame.Components; JpegComponentConfig[] components = frame.Components;
// Length (high byte, low byte), 8 + components * 3. // Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length); int markerlen = 8 + (3 * components.Length);
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen); this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer);
this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported buffer[5] = (byte)components.Length;
this.buffer[1] = (byte)(height >> 8); buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported buffer[1] = (byte)(height >> 8);
this.buffer[3] = (byte)(width >> 8); buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported buffer[3] = (byte)(width >> 8);
this.buffer[5] = (byte)components.Length; buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
// Components data // Components data
for (int i = 0; i < components.Length; i++) for (int i = 0; i < components.Length; i++)
{ {
int i3 = 3 * i; int i3 = 3 * i;
Span<byte> bufferSpan = this.buffer.AsSpan(i3 + 6, 3); Span<byte> bufferSpan = buffer.Slice(i3 + 6, 3);
// Quantization table selector // Quantization table selector
bufferSpan[2] = (byte)components[i].QuantizatioTableIndex; bufferSpan[2] = (byte)components[i].QuantizatioTableIndex;
@ -538,14 +546,15 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
bufferSpan[0] = components[i].Id; 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);
} }
/// <summary> /// <summary>
/// Writes the StartOfScan marker. /// Writes the StartOfScan marker.
/// </summary> /// </summary>
/// <param name="components">The collecction of component configuration items.</param> /// <param name="components">The collecction of component configuration items.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components) /// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer)
{ {
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - 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 // - 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) // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
// should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00. // should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00.
this.buffer[0] = JpegConstants.Markers.XFF; buffer[1] = JpegConstants.Markers.SOS;
this.buffer[1] = JpegConstants.Markers.SOS; buffer[0] = JpegConstants.Markers.XFF;
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan) // Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
int sosSize = 6 + (2 * components.Length); int sosSize = 6 + (2 * components.Length);
this.buffer[2] = 0x00; buffer[4] = (byte)components.Length; // Number of components in a scan
this.buffer[3] = (byte)sosSize; buffer[3] = (byte)sosSize;
this.buffer[4] = (byte)components.Length; // Number of components in a scan buffer[2] = 0x00;
// Components data // Components data
for (int i = 0; i < components.Length; i++) for (int i = 0; i < components.Length; i++)
@ -571,27 +580,28 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
int i2 = 2 * i; int i2 = 2 * i;
// Id // Id
this.buffer[i2 + 5] = components[i].Id; buffer[i2 + 5] = components[i].Id;
// Table selectors // Table selectors
int tableSelectors = (components[i].DcTableSelector << 4) | components[i].AcTableSelector; 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. buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
this.buffer[sosSize] = 0x3f; // Se - End of spectral selection. buffer[sosSize] = 0x3f; // Se - End of spectral selection.
this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(this.buffer, 0, sosSize + 2); this.outputStream.Write(buffer, 0, sosSize + 2);
} }
/// <summary> /// <summary>
/// Writes the EndOfImage marker. /// Writes the EndOfImage marker.
/// </summary> /// </summary>
private void WriteEndOfImageMarker() /// <param name="buffer">Temporary buffer.</param>
private void WriteEndOfImageMarker(Span<byte> buffer)
{ {
this.buffer[0] = JpegConstants.Markers.XFF; buffer[1] = JpegConstants.Markers.EOI;
this.buffer[1] = JpegConstants.Markers.EOI; buffer[0] = JpegConstants.Markers.XFF;
this.outputStream.Write(this.buffer, 0, 2); this.outputStream.Write(buffer, 0, 2);
} }
/// <summary> /// <summary>
@ -602,12 +612,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <param name="frameConfig">The frame configuration.</param> /// <param name="frameConfig">The frame configuration.</param>
/// <param name="spectralConverter">The spectral converter.</param> /// <param name="spectralConverter">The spectral converter.</param>
/// <param name="encoder">The scan encoder.</param> /// <param name="encoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
private void WriteHuffmanScans<TPixel>( private void WriteHuffmanScans<TPixel>(
JpegFrame frame, JpegFrame frame,
JpegFrameConfig frameConfig, JpegFrameConfig frameConfig,
SpectralConverter<TPixel> spectralConverter, SpectralConverter<TPixel> spectralConverter,
HuffmanScanEncoder encoder, HuffmanScanEncoder encoder,
Span<byte> buffer,
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -615,14 +627,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
{ {
frame.AllocateComponents(fullScan: false); frame.AllocateComponents(fullScan: false);
this.WriteStartOfScan(frameConfig.Components); this.WriteStartOfScan(frameConfig.Components, buffer);
encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken); encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken);
} }
else if (frame.Interleaved) else if (frame.Interleaved)
{ {
frame.AllocateComponents(fullScan: false); frame.AllocateComponents(fullScan: false);
this.WriteStartOfScan(frameConfig.Components); this.WriteStartOfScan(frameConfig.Components, buffer);
encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken); encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken);
} }
else else
@ -633,7 +645,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
Span<JpegComponentConfig> components = frameConfig.Components; Span<JpegComponentConfig> components = frameConfig.Components;
for (int i = 0; i < frame.Components.Length; i++) 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); encoder.EncodeScanBaseline(frame.Components[i], cancellationToken);
} }
} }
@ -644,14 +656,16 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <param name="marker">The marker to write.</param> /// <param name="marker">The marker to write.</param>
/// <param name="length">The marker length.</param> /// <param name="length">The marker length.</param>
private void WriteMarkerHeader(byte marker, int length) /// <param name="buffer">Temporary buffer.</param>
private void WriteMarkerHeader(byte marker, int length, Span<byte> buffer)
{ {
// Markers are always prefixed with 0xff. // Markers are always prefixed with 0xff.
this.buffer[0] = JpegConstants.Markers.XFF; buffer[3] = (byte)(length & 0xff);
this.buffer[1] = marker; buffer[2] = (byte)(length >> 8);
this.buffer[2] = (byte)(length >> 8); buffer[1] = marker;
this.buffer[3] = (byte)(length & 0xff); buffer[0] = JpegConstants.Markers.XFF;
this.outputStream.Write(this.buffer, 0, 4);
this.outputStream.Write(buffer, 0, 4);
} }
/// <summary> /// <summary>
@ -668,15 +682,16 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// <param name="configs">Quantization tables configs.</param> /// <param name="configs">Quantization tables configs.</param>
/// <param name="optionsQuality">Optional quality value from the options.</param> /// <param name="optionsQuality">Optional quality value from the options.</param>
/// <param name="metadata">Jpeg metadata instance.</param> /// <param name="metadata">Jpeg metadata instance.</param>
private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata) /// <param name="tmpBuffer">Temporary buffer.</param>
private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata, Span<byte> tmpBuffer)
{ {
int dataLen = configs.Length * (1 + Block8x8.Size); int dataLen = configs.Length * (1 + Block8x8.Size);
// Marker + quantization table lengths. // Marker + quantization table lengths.
int markerlen = 2 + dataLen; int markerlen = 2 + dataLen;
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen, tmpBuffer);
byte[] buffer = new byte[dataLen]; Span<byte> buffer = dataLen <= 256 ? stackalloc byte[dataLen] : new byte[dataLen];
int offset = 0; int offset = 0;
Block8x8F workspaceBlock = default; Block8x8F workspaceBlock = default;

57
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -28,11 +28,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary> /// </summary>
internal sealed class PngDecoderCore : IImageDecoderInternals internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary> /// <summary>
/// The general decoder options. /// The general decoder options.
/// </summary> /// </summary>
@ -154,9 +149,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.currentStream = stream; this.currentStream = stream;
this.currentStream.Skip(8); this.currentStream.Skip(8);
Image<TPixel> image = null; Image<TPixel> image = null;
Span<byte> buffer = stackalloc byte[20];
try try
{ {
while (this.TryReadChunk(out PngChunk chunk)) while (this.TryReadChunk(buffer, out PngChunk chunk))
{ {
try try
{ {
@ -252,10 +249,13 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ImageMetadata metadata = new(); ImageMetadata metadata = new();
PngMetadata pngMetadata = metadata.GetPngMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream; this.currentStream = stream;
Span<byte> buffer = stackalloc byte[20];
this.currentStream.Skip(8); this.currentStream.Skip(8);
try try
{ {
while (this.TryReadChunk(out PngChunk chunk)) while (this.TryReadChunk(buffer, out PngChunk chunk))
{ {
try try
{ {
@ -1401,9 +1401,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
return 0; return 0;
} }
this.currentStream.Read(this.buffer, 0, 4); Span<byte> 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) if (chunk.Type == PngChunkType.Data)
{ {
@ -1420,11 +1422,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads a chunk from the stream. /// Reads a chunk from the stream.
/// </summary> /// </summary>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="chunk">The image format chunk.</param> /// <param name="chunk">The image format chunk.</param>
/// <returns> /// <returns>
/// The <see cref="PngChunk"/>. /// The <see cref="PngChunk"/>.
/// </returns> /// </returns>
private bool TryReadChunk(out PngChunk chunk) private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
{ {
if (this.nextChunk != null) if (this.nextChunk != null)
{ {
@ -1435,7 +1438,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
return true; return true;
} }
if (!this.TryReadChunkLength(out int length)) if (!this.TryReadChunkLength(buffer, out int length))
{ {
chunk = default; chunk = default;
@ -1446,7 +1449,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
while (length < 0 || length > (this.currentStream.Length - this.currentStream.Position)) while (length < 0 || length > (this.currentStream.Length - this.currentStream.Position))
{ {
// Not a valid chunk so try again until we reach a known chunk. // 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; chunk = default;
@ -1454,7 +1457,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
} }
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. // 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. // We can skip all other chunk data in the stream for better performance.
@ -1471,7 +1474,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
type: type, type: type,
data: this.ReadChunkData(length)); 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 // Restore the stream position for IDAT chunks, because it will be decoded later and
// was only read to verifying the CRC is correct. // was only read to verifying the CRC is correct.
@ -1487,9 +1490,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// Validates the png chunk. /// Validates the png chunk.
/// </summary> /// </summary>
/// <param name="chunk">The <see cref="PngChunk"/>.</param> /// <param name="chunk">The <see cref="PngChunk"/>.</param>
private void ValidateChunk(in PngChunk chunk) /// <param name="buffer">Temporary buffer.</param>
private void ValidateChunk(in PngChunk chunk, Span<byte> buffer)
{ {
uint inputCrc = this.ReadChunkCrc(); uint inputCrc = this.ReadChunkCrc(buffer);
if (chunk.IsCritical) if (chunk.IsCritical)
{ {
@ -1513,13 +1517,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the cycle redundancy chunk from the data. /// Reads the cycle redundancy chunk from the data.
/// </summary> /// </summary>
/// <param name="buffer">Temporary buffer.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private uint ReadChunkCrc() private uint ReadChunkCrc(Span<byte> buffer)
{ {
uint crc = 0; 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; return crc;
@ -1554,15 +1559,16 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Identifies the chunk type from the chunk. /// Identifies the chunk type from the chunk.
/// </summary> /// </summary>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid. /// Thrown if the input stream is not valid.
/// </exception> /// </exception>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private PngChunkType ReadChunkType() private PngChunkType ReadChunkType(Span<byte> 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(); PngThrowHelper.ThrowInvalidChunkType();
@ -1574,16 +1580,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Attempts to read the length of the next chunk. /// Attempts to read the length of the next chunk.
/// </summary> /// </summary>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="result">The result length. If the return type is <see langword="false"/> this parameter is passed uninitialized.</param> /// <param name="result">The result length. If the return type is <see langword="false"/> this parameter is passed uninitialized.</param>
/// <returns> /// <returns>
/// Whether the length was read. /// Whether the length was read.
/// </returns> /// </returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private bool TryReadChunkLength(out int result) private bool TryReadChunkLength(Span<byte> 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; return true;
} }

51
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -38,15 +38,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
private readonly Configuration configuration; private readonly Configuration configuration;
/// <summary>
/// Reusable buffer for writing general data.
/// </summary>
private readonly byte[] buffer = new byte[8];
/// <summary> /// <summary>
/// Reusable buffer for writing chunk data. /// Reusable buffer for writing chunk data.
/// </summary> /// </summary>
private readonly byte[] chunkDataBuffer = new byte[16]; private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly
/// <summary> /// <summary>
/// The encoder with options /// The encoder with options
@ -576,9 +571,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
filterMethod: 0, filterMethod: 0,
interlaceMethod: this.interlaceMode); 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);
} }
/// <summary> /// <summary>
@ -652,9 +647,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; 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);
} }
/// <summary> /// <summary>
@ -880,9 +875,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// 4-byte unsigned integer of gamma * 100,000. // 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F); 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; return;
} }
Span<byte> alpha = this.chunkDataBuffer.AsSpan(); Span<byte> alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb) if (pngMetadata.ColorType == PngColorType.Rgb)
{ {
if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit)
@ -909,7 +904,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); 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) else if (pngMetadata.TransparentRgb24.HasValue)
{ {
@ -918,7 +913,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
alpha[1] = rgb.R; alpha[1] = rgb.R;
alpha[3] = rgb.G; alpha[3] = rgb.G;
alpha[5] = rgb.B; 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) else if (pngMetadata.ColorType == PngColorType.Grayscale)
@ -926,13 +921,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (pngMetadata.TransparentL16.HasValue && this.use16Bit) if (pngMetadata.TransparentL16.HasValue && this.use16Bit)
{ {
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); 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) else if (pngMetadata.TransparentL8.HasValue)
{ {
alpha.Clear(); alpha.Clear();
alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; 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 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="length">The of the data to write.</param> /// <param name="length">The of the data to write.</param>
private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int offset, int length) private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int offset, int length)
{ {
BinaryPrimitives.WriteInt32BigEndian(this.buffer, length); Span<byte> buffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt32BigEndian(this.buffer.AsSpan(4, 4), (uint)type);
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) if (data.Length > 0 && length > 0)
{ {
@ -1187,9 +1184,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
crc = Crc32.Calculate(crc, data.Slice(offset, length)); 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
} }
/// <summary> /// <summary>
@ -1412,4 +1409,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16, Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8 _ => PngBitDepth.Bit8
}; };
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);
}
} }

43
src/ImageSharp/Formats/Tga/TgaDecoderCore.cs

@ -17,11 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// </summary> /// </summary>
internal sealed class TgaDecoderCore : IImageDecoderInternals internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] scratchBuffer = new byte[4];
/// <summary> /// <summary>
/// General configuration options. /// General configuration options.
/// </summary> /// </summary>
@ -407,6 +402,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
bool invertX = InvertX(origin); bool invertX = InvertX(origin);
using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0); using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 2, 0);
Span<byte> rowSpan = row.GetSpan(); Span<byte> rowSpan = row.GetSpan();
Span<byte> scratchBuffer = stackalloc byte[2];
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
@ -417,7 +413,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
int bytesRead = stream.Read(this.scratchBuffer, 0, 2); int bytesRead = stream.Read(scratchBuffer);
if (bytesRead != 2) if (bytesRead != 2)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -425,16 +421,16 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
if (!this.hasAlpha) if (!this.hasAlpha)
{ {
this.scratchBuffer[1] |= 1 << 7; scratchBuffer[1] |= 1 << 7;
} }
if (this.fileHeader.ImageType == TgaImageType.BlackAndWhite) if (this.fileHeader.ImageType == TgaImageType.BlackAndWhite)
{ {
color.FromLa16(Unsafe.As<byte, La16>(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); color.FromLa16(Unsafe.As<byte, La16>(ref MemoryMarshal.GetReference(scratchBuffer)));
} }
else else
{ {
color.FromBgra5551(Unsafe.As<byte, Bgra5551>(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); color.FromBgra5551(Unsafe.As<byte, Bgra5551>(ref MemoryMarshal.GetReference(scratchBuffer)));
} }
pixelSpan[x] = color; pixelSpan[x] = color;
@ -484,6 +480,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
bool invertX = InvertX(origin); bool invertX = InvertX(origin);
if (invertX) if (invertX)
{ {
Span<byte> scratchBuffer = stackalloc byte[4];
TPixel color = default; TPixel color = default;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
@ -491,7 +488,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadBgr24Pixel(stream, color, x, pixelSpan); ReadBgr24Pixel(stream, color, x, pixelSpan, scratchBuffer);
} }
} }
@ -558,6 +555,8 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
return; return;
} }
Span<byte> scratchBuffer = stackalloc byte[4];
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
int newY = InvertY(y, height, origin); int newY = InvertY(y, height, origin);
@ -566,14 +565,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadBgra32Pixel(stream, x, color, pixelRow); this.ReadBgra32Pixel(stream, x, color, pixelRow, scratchBuffer);
} }
} }
else else
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
this.ReadBgra32Pixel(stream, x, color, pixelRow); this.ReadBgra32Pixel(stream, x, color, pixelRow, scratchBuffer);
} }
} }
} }
@ -687,16 +686,16 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgr24Pixel<TPixel>(BufferedReadStream stream, TPixel color, int x, Span<TPixel> pixelSpan) private static void ReadBgr24Pixel<TPixel>(BufferedReadStream stream, TPixel color, int x, Span<TPixel> pixelSpan, Span<byte> scratchBuffer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = stream.Read(this.scratchBuffer, 0, 3); int bytesRead = stream.Read(scratchBuffer, 0, 3);
if (bytesRead != 3) if (bytesRead != 3)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel");
} }
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref MemoryMarshal.GetArrayDataReference(this.scratchBuffer))); color.FromBgr24(Unsafe.As<byte, Bgr24>(ref MemoryMarshal.GetReference(scratchBuffer)));
pixelSpan[x] = color; pixelSpan[x] = color;
} }
@ -715,10 +714,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgra32Pixel<TPixel>(BufferedReadStream stream, int x, TPixel color, Span<TPixel> pixelRow) private void ReadBgra32Pixel<TPixel>(BufferedReadStream stream, int x, TPixel color, Span<TPixel> pixelRow, Span<byte> scratchBuffer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = stream.Read(this.scratchBuffer, 0, 4); int bytesRead = stream.Read(scratchBuffer, 0, 4);
if (bytesRead != 4) if (bytesRead != 4)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel");
@ -726,8 +725,8 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
Guard.NotNull(this.tgaMetadata); Guard.NotNull(this.tgaMetadata);
byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : this.scratchBuffer[3]; byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : scratchBuffer[3];
color.FromBgra32(new Bgra32(this.scratchBuffer[2], this.scratchBuffer[1], this.scratchBuffer[0], alpha)); color.FromBgra32(new Bgra32(scratchBuffer[2], scratchBuffer[1], scratchBuffer[0], alpha));
pixelRow[x] = color; pixelRow[x] = color;
} }
@ -814,7 +813,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
private void UncompressRle(BufferedReadStream stream, int width, int height, Span<byte> buffer, int bytesPerPixel) private void UncompressRle(BufferedReadStream stream, int width, int height, Span<byte> buffer, int bytesPerPixel)
{ {
int uncompressedPixels = 0; int uncompressedPixels = 0;
Span<byte> pixel = this.scratchBuffer.AsSpan(0, bytesPerPixel); Span<byte> pixel = stackalloc byte[bytesPerPixel];
int totalPixels = width * height; int totalPixels = width * height;
while (uncompressedPixels < totalPixels) while (uncompressedPixels < totalPixels)
{ {
@ -825,7 +824,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
if (highBit == 1) if (highBit == 1)
{ {
int runLength = runLengthByte & 127; int runLength = runLengthByte & 127;
int bytesRead = stream.Read(pixel, 0, bytesPerPixel); int bytesRead = stream.Read(pixel);
if (bytesRead != bytesPerPixel) if (bytesRead != bytesPerPixel)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream");
@ -845,7 +844,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
int bufferIdx = uncompressedPixels * bytesPerPixel; int bufferIdx = uncompressedPixels * bytesPerPixel;
for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) for (int i = 0; i < runLength + 1; i++, uncompressedPixels++)
{ {
int bytesRead = stream.Read(pixel, 0, bytesPerPixel); int bytesRead = stream.Read(pixel);
if (bytesRead != bytesPerPixel) if (bytesRead != bytesPerPixel)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream");

12
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -22,11 +22,6 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// Reusable buffer for writing data.
/// </summary>
private readonly byte[] buffer = new byte[2];
/// <summary> /// <summary>
/// The color depth, in number of bits per pixel. /// The color depth, in number of bits per pixel.
/// </summary> /// </summary>
@ -221,9 +216,10 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
case TgaBitsPerPixel.Pixel16: case TgaBitsPerPixel.Pixel16:
Bgra5551 bgra5551 = new(color.ToVector4()); Bgra5551 bgra5551 = new(color.ToVector4());
BinaryPrimitives.WriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); Span<byte> buffer = stackalloc byte[2];
stream.WriteByte(this.buffer[0]); BinaryPrimitives.WriteInt16LittleEndian(buffer, (short)bgra5551.PackedValue);
stream.WriteByte(this.buffer[1]); stream.WriteByte(buffer[0]);
stream.WriteByte(buffer[1]);
break; break;

11
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs

@ -24,9 +24,9 @@ internal class BlackIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
/// <inheritdoc/> /// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height) public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{ {
var color = default(TPixel); TPixel color = default;
color.FromScaledVector4(Vector4.Zero); color.FromScaledVector4(Vector4.Zero);
byte[] buffer = new byte[4]; Span<byte> buffer = stackalloc byte[4];
int offset = 0; int offset = 0;
for (int y = top; y < top + height; y++) for (int y = top; y < top + height; y++)
@ -37,8 +37,8 @@ internal class BlackIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float intensity = BitConverter.ToSingle(buffer, 0); float intensity = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); var colorVector = new Vector4(intensity, intensity, intensity, 1.0f);
@ -50,8 +50,7 @@ internal class BlackIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
{ {
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); float intensity = BitConverter.ToSingle(data.Slice(offset, 4));
float intensity = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); var colorVector = new Vector4(intensity, intensity, intensity, 1.0f);

23
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs

@ -27,7 +27,7 @@ internal class RgbFloat323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
var color = default(TPixel); var color = default(TPixel);
color.FromScaledVector4(Vector4.Zero); color.FromScaledVector4(Vector4.Zero);
int offset = 0; int offset = 0;
byte[] buffer = new byte[4]; Span<byte> buffer = stackalloc byte[4];
for (int y = top; y < top + height; y++) for (int y = top; y < top + height; y++)
{ {
@ -38,18 +38,18 @@ internal class RgbFloat323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float r = BitConverter.ToSingle(buffer, 0); float r = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float g = BitConverter.ToSingle(buffer, 0); float g = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float b = BitConverter.ToSingle(buffer, 0); float b = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
var colorVector = new Vector4(r, g, b, 1.0f); var colorVector = new Vector4(r, g, b, 1.0f);
@ -61,16 +61,13 @@ internal class RgbFloat323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
{ {
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); float r = BitConverter.ToSingle(data.Slice(offset, 4));
float r = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); float g = BitConverter.ToSingle(data.Slice(offset, 4));
float g = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); float b = BitConverter.ToSingle(data.Slice(offset, 4));
float b = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
var colorVector = new Vector4(r, g, b, 1.0f); var colorVector = new Vector4(r, g, b, 1.0f);

30
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs

@ -27,7 +27,7 @@ internal class RgbaFloat32323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
var color = default(TPixel); var color = default(TPixel);
color.FromScaledVector4(Vector4.Zero); color.FromScaledVector4(Vector4.Zero);
int offset = 0; int offset = 0;
byte[] buffer = new byte[4]; Span<byte> buffer = stackalloc byte[4];
for (int y = top; y < top + height; y++) for (int y = top; y < top + height; y++)
{ {
@ -38,23 +38,23 @@ internal class RgbaFloat32323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float r = BitConverter.ToSingle(buffer, 0); float r = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float g = BitConverter.ToSingle(buffer, 0); float g = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float b = BitConverter.ToSingle(buffer, 0); float b = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float a = BitConverter.ToSingle(buffer, 0); float a = BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
var colorVector = new Vector4(r, g, b, a); var colorVector = new Vector4(r, g, b, a);
@ -66,20 +66,16 @@ internal class RgbaFloat32323232TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
{ {
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); float r = BitConverter.ToSingle(data.Slice(offset, 4));
float r = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); float g = BitConverter.ToSingle(data.Slice(offset, 4));
float g = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); float b = BitConverter.ToSingle(data.Slice(offset, 4));
float b = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
data.Slice(offset, 4).CopyTo(buffer); float a = BitConverter.ToSingle(data.Slice(offset, 4));
float a = BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
var colorVector = new Vector4(r, g, b, a); var colorVector = new Vector4(r, g, b, a);

9
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs

@ -26,7 +26,7 @@ internal class WhiteIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
{ {
var color = default(TPixel); var color = default(TPixel);
color.FromScaledVector4(Vector4.Zero); color.FromScaledVector4(Vector4.Zero);
byte[] buffer = new byte[4]; Span<byte> buffer = stackalloc byte[4];
int offset = 0; int offset = 0;
for (int y = top; y < top + height; y++) for (int y = top; y < top + height; y++)
@ -37,8 +37,8 @@ internal class WhiteIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); data.Slice(offset, 4).CopyTo(buffer);
Array.Reverse(buffer); buffer.Reverse();
float intensity = 1.0f - BitConverter.ToSingle(buffer, 0); float intensity = 1.0f - BitConverter.ToSingle(buffer);
offset += 4; offset += 4;
var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); var colorVector = new Vector4(intensity, intensity, intensity, 1.0f);
@ -50,8 +50,7 @@ internal class WhiteIsZero32FloatTiffColor<TPixel> : TiffBaseColorDecoder<TPixel
{ {
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {
data.Slice(offset, 4).CopyTo(buffer); float intensity = 1.0f - BitConverter.ToSingle(data.Slice(offset, 4));
float intensity = 1.0f - BitConverter.ToSingle(buffer, 0);
offset += 4; offset += 4;
var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); var colorVector = new Vector4(intensity, intensity, intensity, 1.0f);

38
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -29,11 +29,6 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary> /// <summary>
/// The global configuration. /// The global configuration.
/// </summary> /// </summary>
@ -157,7 +152,9 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor);
using TiffStreamWriter writer = new(stream); using TiffStreamWriter writer = new(stream);
long ifdMarker = WriteHeader(writer); Span<byte> buffer = stackalloc byte[4];
long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel> metadataImage = image; Image<TPixel> metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
@ -171,7 +168,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
long currentOffset = writer.BaseStream.Position; long currentOffset = writer.BaseStream.Position;
foreach ((long, uint) marker in this.frameMarkers) 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); writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin);
@ -181,14 +178,15 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
/// Writes the TIFF file header. /// Writes the TIFF file header.
/// </summary> /// </summary>
/// <param name="writer">The <see cref="TiffStreamWriter" /> to write data to.</param> /// <param name="writer">The <see cref="TiffStreamWriter" /> to write data to.</param>
/// <param name="buffer">Scratch buffer with minimum size of 2.</param>
/// <returns> /// <returns>
/// The marker to write the first IFD offset. /// The marker to write the first IFD offset.
/// </returns> /// </returns>
public static long WriteHeader(TiffStreamWriter writer) public static long WriteHeader(TiffStreamWriter writer, Span<byte> buffer)
{ {
writer.Write(ByteOrderMarker); writer.Write(ByteOrderMarker, buffer);
writer.Write(TiffConstants.HeaderMagicNumber); writer.Write(TiffConstants.HeaderMagicNumber, buffer);
return writer.PlaceMarker(); return writer.PlaceMarker(buffer);
} }
/// <summary> /// <summary>
@ -307,20 +305,22 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag);
writer.Write((ushort)entries.Count); Span<byte> buffer = stackalloc byte[4];
writer.Write((ushort)entries.Count, buffer);
foreach (IExifValue entry in entries) foreach (IExifValue entry in entries)
{ {
writer.Write((ushort)entry.Tag); writer.Write((ushort)entry.Tag, buffer);
writer.Write((ushort)entry.DataType); writer.Write((ushort)entry.DataType, buffer);
writer.Write(ExifWriter.GetNumberOfComponents(entry)); writer.Write(ExifWriter.GetNumberOfComponents(entry), buffer);
uint length = ExifWriter.GetLength(entry); uint length = ExifWriter.GetLength(entry);
if (length <= 4) 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"); DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(this.buffer.AsSpan(0, sz)); writer.WritePadded(buffer.Slice(0, sz));
} }
else else
{ {
@ -328,12 +328,12 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
int sz = ExifWriter.WriteValue(entry, raw, 0); int sz = ExifWriter.WriteValue(entry, raw, 0);
DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written");
largeDataBlocks.Add(raw); largeDataBlocks.Add(raw);
writer.Write(dataOffset); writer.Write(dataOffset, buffer);
dataOffset += (uint)(raw.Length + (raw.Length % 2)); dataOffset += (uint)(raw.Length + (raw.Length % 2));
} }
} }
long nextIfdMarker = writer.PlaceMarker(); long nextIfdMarker = writer.PlaceMarker(buffer);
foreach (byte[] dataBlock in largeDataBlocks) foreach (byte[] dataBlock in largeDataBlocks)
{ {

44
src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs

@ -10,13 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
/// </summary> /// </summary>
internal sealed class TiffStreamWriter : IDisposable internal sealed class TiffStreamWriter : IDisposable
{ {
private static readonly byte[] PaddingBytes = new byte[4];
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffStreamWriter"/> class. /// Initializes a new instance of the <see cref="TiffStreamWriter"/> class.
/// </summary> /// </summary>
@ -41,11 +34,12 @@ internal sealed class TiffStreamWriter : IDisposable
/// <summary> /// <summary>
/// Writes an empty four bytes to the stream, returning the offset to be written later. /// Writes an empty four bytes to the stream, returning the offset to be written later.
/// </summary> /// </summary>
/// <param name="buffer">Scratch buffer with minimum size of 4.</param>
/// <returns>The offset to be written later.</returns> /// <returns>The offset to be written later.</returns>
public long PlaceMarker() public long PlaceMarker(Span<byte> buffer)
{ {
long offset = this.BaseStream.Position; long offset = this.BaseStream.Position;
this.Write(0u); this.Write(0u, buffer);
return offset; return offset;
} }
@ -71,36 +65,38 @@ internal sealed class TiffStreamWriter : IDisposable
/// Writes a two-byte unsigned integer to the current stream. /// Writes a two-byte unsigned integer to the current stream.
/// </summary> /// </summary>
/// <param name="value">The two-byte unsigned integer to write.</param> /// <param name="value">The two-byte unsigned integer to write.</param>
public void Write(ushort value) /// <param name="buffer">Scratch buffer with minimum size of 2.</param>
public void Write(ushort value, Span<byte> buffer)
{ {
if (IsLittleEndian) if (IsLittleEndian)
{ {
BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, value); BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
} }
else 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));
} }
/// <summary> /// <summary>
/// Writes a four-byte unsigned integer to the current stream. /// Writes a four-byte unsigned integer to the current stream.
/// </summary> /// </summary>
/// <param name="value">The four-byte unsigned integer to write.</param> /// <param name="value">The four-byte unsigned integer to write.</param>
public void Write(uint value) /// <param name="buffer">Scratch buffer with minimum size of 4.</param>
public void Write(uint value, Span<byte> buffer)
{ {
if (IsLittleEndian) if (IsLittleEndian)
{ {
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
} }
else 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));
} }
/// <summary> /// <summary>
@ -113,7 +109,10 @@ internal sealed class TiffStreamWriter : IDisposable
if (value.Length % 4 != 0) 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<byte> paddingBytes = new byte[4] { 0x00, 0x00, 0x00, 0x00 };
paddingBytes = paddingBytes[..(4 - (value.Length % 4))];
this.BaseStream.Write(paddingBytes);
} }
} }
@ -122,18 +121,19 @@ internal sealed class TiffStreamWriter : IDisposable
/// </summary> /// </summary>
/// <param name="offset">The offset returned when placing the marker</param> /// <param name="offset">The offset returned when placing the marker</param>
/// <param name="value">The four-byte unsigned integer to write.</param> /// <param name="value">The four-byte unsigned integer to write.</param>
public void WriteMarker(long offset, uint value) /// <param name="buffer">Scratch buffer.</param>
public void WriteMarker(long offset, uint value, Span<byte> buffer)
{ {
long back = this.BaseStream.Position; long back = this.BaseStream.Position;
this.BaseStream.Seek(offset, SeekOrigin.Begin); this.BaseStream.Seek(offset, SeekOrigin.Begin);
this.Write(value); this.Write(value, buffer);
this.BaseStream.Seek(back, SeekOrigin.Begin); this.BaseStream.Seek(back, SeekOrigin.Begin);
} }
public void WriteMarkerFast(long offset, uint value) public void WriteMarkerFast(long offset, uint value, Span<byte> buffer)
{ {
this.BaseStream.Seek(offset, SeekOrigin.Begin); this.BaseStream.Seek(offset, SeekOrigin.Begin);
this.Write(value); this.Write(value, buffer);
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs

@ -192,7 +192,7 @@ internal class Vp8LBitReader : BitReaderBase
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private void ShiftBytes() private void ShiftBytes()
{ {
System.Span<byte> dataSpan = this.Data!.Memory.Span; Span<byte> dataSpan = this.Data!.Memory.Span;
while (this.bitPos >= 8 && this.pos < this.len) while (this.bitPos >= 8 && this.pos < this.len)
{ {
this.value >>= 8; this.value >>= 8;

23
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@ -23,7 +24,7 @@ internal abstract class BitWriterBase
/// <summary> /// <summary>
/// A scratch buffer to reduce allocations. /// A scratch buffer to reduce allocations.
/// </summary> /// </summary>
private readonly byte[] scratchBuffer = new byte[4]; private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BitWriterBase"/> class. /// Initializes a new instance of the <see cref="BitWriterBase"/> class.
@ -90,8 +91,8 @@ internal abstract class BitWriterBase
protected void WriteRiffHeader(Stream stream, uint riffSize) protected void WriteRiffHeader(Stream stream, uint riffSize)
{ {
stream.Write(WebpConstants.RiffFourCc); stream.Write(WebpConstants.RiffFourCc);
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, riffSize); BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
stream.Write(this.scratchBuffer.AsSpan(0, 4)); stream.Write(this.scratchBuffer.Span.Slice(0, 4));
stream.Write(WebpConstants.WebpHeader); stream.Write(WebpConstants.WebpHeader);
} }
@ -128,7 +129,7 @@ internal abstract class BitWriterBase
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)metadataBytes.Length; uint size = (uint)metadataBytes.Length;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.Span.Slice(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
stream.Write(buf); stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size); BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@ -151,7 +152,7 @@ internal abstract class BitWriterBase
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed) protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{ {
uint size = (uint)dataBytes.Length + 1; uint size = (uint)dataBytes.Length + 1;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.Span.Slice(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf); stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size); BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@ -182,7 +183,7 @@ internal abstract class BitWriterBase
{ {
uint size = (uint)iccProfileBytes.Length; uint size = (uint)iccProfileBytes.Length;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.Span.Slice(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp);
stream.Write(buf); stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size); BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@ -245,7 +246,7 @@ internal abstract class BitWriterBase
flags |= 32; flags |= 32;
} }
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.Span.Slice(0, 4);
stream.Write(WebpConstants.Vp8XMagicBytes); stream.Write(WebpConstants.Vp8XMagicBytes);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
stream.Write(buf); stream.Write(buf);
@ -256,4 +257,12 @@ internal abstract class BitWriterBase
BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1);
stream.Write(buf[..3]); stream.Write(buf[..3]);
} }
private unsafe struct ScratchBuffer
{
private const int Size = 4;
private fixed byte scratch[Size];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
} }

15
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -14,11 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
/// </summary> /// </summary>
internal class Vp8LBitWriter : BitWriterBase internal class Vp8LBitWriter : BitWriterBase
{ {
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] scratchBuffer = new byte[8];
/// <summary> /// <summary>
/// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed. /// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed.
/// </summary> /// </summary>
@ -194,8 +189,9 @@ internal class Vp8LBitWriter : BitWriterBase
stream.Write(WebpConstants.Vp8LMagicBytes); stream.Write(WebpConstants.Vp8LMagicBytes);
// Write Vp8 Header. // Write Vp8 Header.
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, size); Span<byte> scratchBuffer = stackalloc byte[8];
stream.Write(this.scratchBuffer.AsSpan(0, 4)); BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size);
stream.Write(scratchBuffer.Slice(0, 4));
stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte);
// Write the encoded bytes of the image to the stream. // Write the encoded bytes of the image to the stream.
@ -228,8 +224,9 @@ internal class Vp8LBitWriter : BitWriterBase
this.BitWriterResize(extraSize); this.BitWriterResize(extraSize);
} }
BinaryPrimitives.WriteUInt64LittleEndian(this.scratchBuffer, this.bits); Span<byte> scratchBuffer = stackalloc byte[8];
this.scratchBuffer.AsSpan(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits);
scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur));
this.cur += WriterBytes; this.cur += WriterBytes;
this.bits >>= WriterBits; this.bits >>= WriterBits;

32
src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs

@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless; namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal class HistogramEncoder internal static class HistogramEncoder
{ {
/// <summary> /// <summary>
/// Number of partitions for the three dominant (literal, red and blue) symbol costs. /// 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; private const ushort InvalidHistogramSymbol = ushort.MaxValue;
public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List<Vp8LHistogram> imageHisto, Vp8LHistogram tmpHisto, ushort[] histogramSymbols) public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List<Vp8LHistogram> imageHisto, Vp8LHistogram tmpHisto, Span<ushort> histogramSymbols)
{ {
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1;
int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1;
@ -148,7 +148,7 @@ internal class HistogramEncoder
} }
} }
private static int HistogramCopyAndAnalyze(List<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, ushort[] histogramSymbols) private static int HistogramCopyAndAnalyze(List<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, Span<ushort> histogramSymbols)
{ {
var stats = new Vp8LStreaks(); var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy(); var bitsEntropy = new Vp8LBitEntropy();
@ -171,20 +171,28 @@ internal class HistogramEncoder
} }
} }
int numUsed = histogramSymbols.Count(h => h != InvalidHistogramSymbol); int numUsed = 0;
foreach (ushort h in histogramSymbols)
{
if (h != InvalidHistogramSymbol)
{
numUsed++;
}
}
return numUsed; return numUsed;
} }
private static void HistogramCombineEntropyBin( private static void HistogramCombineEntropyBin(
List<Vp8LHistogram> histograms, List<Vp8LHistogram> histograms,
ushort[] clusters, Span<ushort> clusters,
ushort[] clusterMappings, ushort[] clusterMappings,
Vp8LHistogram curCombo, Vp8LHistogram curCombo,
ushort[] binMap, ushort[] binMap,
int numBins, int numBins,
double combineCostFactor) double combineCostFactor)
{ {
var binInfo = new HistogramBinInfo[BinSize]; Span<HistogramBinInfo> binInfo = stackalloc HistogramBinInfo[BinSize];
for (int idx = 0; idx < numBins; idx++) for (int idx = 0; idx < numBins; idx++)
{ {
binInfo[idx].First = -1; binInfo[idx].First = -1;
@ -258,7 +266,7 @@ internal class HistogramEncoder
/// Given a Histogram set, the mapping of clusters 'clusterMapping' and the /// 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. /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values.
/// </summary> /// </summary>
private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, ushort[] symbols) private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span<ushort> symbols)
{ {
bool doContinue = true; bool doContinue = true;
@ -331,7 +339,7 @@ internal class HistogramEncoder
int maxSize = 9; int maxSize = 9;
// Fill the initial mapping. // Fill the initial mapping.
int[] mappings = new int[histograms.Count]; Span<int> mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count];
for (int j = 0, iter = 0; iter < histograms.Count; iter++) for (int j = 0, iter = 0; iter < histograms.Count; iter++)
{ {
if (histograms[iter] == null) if (histograms[iter] == null)
@ -388,9 +396,9 @@ internal class HistogramEncoder
int bestIdx1 = histoPriorityList[0].Idx1; int bestIdx1 = histoPriorityList[0].Idx1;
int bestIdx2 = histoPriorityList[0].Idx2; int bestIdx2 = histoPriorityList[0].Idx2;
int mappingIndex = Array.IndexOf(mappings, bestIdx2); int mappingIndex = mappings.IndexOf(bestIdx2);
Span<int> src = mappings.AsSpan(mappingIndex + 1, numUsed - mappingIndex - 1); Span<int> src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1);
Span<int> dst = mappings.AsSpan(mappingIndex); Span<int> dst = mappings.Slice(mappingIndex);
src.CopyTo(dst); src.CopyTo(dst);
// Merge the histograms and remove bestIdx2 from the list. // Merge the histograms and remove bestIdx2 from the list.
@ -528,7 +536,7 @@ internal class HistogramEncoder
} }
} }
private static void HistogramRemap(List<Vp8LHistogram> input, List<Vp8LHistogram> output, ushort[] symbols) private static void HistogramRemap(List<Vp8LHistogram> input, List<Vp8LHistogram> output, Span<ushort> symbols)
{ {
int inSize = input.Count; int inSize = input.Count;
int outSize = output.Count; int outSize = output.Count;

26
src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs

@ -25,7 +25,7 @@ internal static class HuffmanUtils
0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf 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<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
{ {
int numSymbols = huffCode.NumSymbols; int numSymbols = huffCode.NumSymbols;
bufRle.AsSpan().Clear(); bufRle.AsSpan().Clear();
@ -159,7 +159,7 @@ internal static class HuffmanUtils
/// <param name="histogramSize">The size of the histogram.</param> /// <param name="histogramSize">The size of the histogram.</param>
/// <param name="treeDepthLimit">The tree depth limit.</param> /// <param name="treeDepthLimit">The tree depth limit.</param>
/// <param name="bitDepths">How many bits are used for the symbol.</param> /// <param name="bitDepths">How many bits are used for the symbol.</param>
public static void GenerateOptimalTree(HuffmanTree[] tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) public static void GenerateOptimalTree(Span<HuffmanTree> tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
{ {
uint countMin; uint countMin;
int treeSizeOrig = 0; int treeSizeOrig = 0;
@ -177,7 +177,7 @@ internal static class HuffmanUtils
return; return;
} }
Span<HuffmanTree> treePool = tree.AsSpan(treeSizeOrig); Span<HuffmanTree> treePool = tree.Slice(treeSizeOrig);
// For block sizes with less than 64k symbols we never need to do a // For block sizes with less than 64k symbols we never need to do a
// second iteration of this loop. // second iteration of this loop.
@ -202,14 +202,8 @@ internal static class HuffmanUtils
} }
// Build the Huffman tree. // Build the Huffman tree.
#if NET5_0_OR_GREATER Span<HuffmanTree> treeSlice = tree.Slice(0, treeSize);
Span<HuffmanTree> treeSlice = tree.AsSpan(0, treeSize);
treeSlice.Sort(HuffmanTree.Compare); 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) if (treeSize > 1)
{ {
@ -312,12 +306,12 @@ internal static class HuffmanUtils
DebugGuard.MustBeGreaterThan(codeLengthsSize, 0, nameof(codeLengthsSize)); DebugGuard.MustBeGreaterThan(codeLengthsSize, 0, nameof(codeLengthsSize));
// sorted[codeLengthsSize] is a pre-allocated array for sorting symbols by code length. // sorted[codeLengthsSize] is a pre-allocated array for sorting symbols by code length.
int[] sorted = new int[codeLengthsSize]; Span<int> sorted = codeLengthsSize <= 64 ? stackalloc int[codeLengthsSize] : new int[codeLengthsSize];
int totalSize = 1 << rootBits; // total size root table + 2nd level table. int totalSize = 1 << rootBits; // total size root table + 2nd level table.
int len; // current code length. int len; // current code length.
int symbol; // symbol index in original or sorted table. int symbol; // symbol index in original or sorted table.
int[] counts = new int[WebpConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. Span<int> counts = stackalloc 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<int> offsets = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length.
// Build histogram of code lengths. // Build histogram of code lengths.
for (symbol = 0; symbol < codeLengthsSize; ++symbol) for (symbol = 0; symbol < codeLengthsSize; ++symbol)
@ -544,8 +538,8 @@ internal static class HuffmanUtils
private static void ConvertBitDepthsToSymbols(HuffmanTreeCode tree) private static void ConvertBitDepthsToSymbols(HuffmanTreeCode tree)
{ {
// 0 bit-depth means that the symbol does not exist. // 0 bit-depth means that the symbol does not exist.
uint[] nextCode = new uint[WebpConstants.MaxAllowedCodeLength + 1]; Span<uint> nextCode = stackalloc uint[WebpConstants.MaxAllowedCodeLength + 1];
int[] depthCount = new int[WebpConstants.MaxAllowedCodeLength + 1]; Span<int> depthCount = stackalloc int[WebpConstants.MaxAllowedCodeLength + 1];
int len = tree.NumSymbols; int len = tree.NumSymbols;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
@ -603,7 +597,7 @@ internal static class HuffmanUtils
/// Returns the table width of the next 2nd level table. count is the histogram of bit lengths for the remaining symbols, /// 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. /// len is the code length of the next processed symbol.
/// </summary> /// </summary>
private static int NextTableBitSize(int[] count, int len, int rootBits) private static int NextTableBitSize(ReadOnlySpan<int> count, int len, int rootBits)
{ {
int left = 1 << (len - rootBits); int left = 1 << (len - rootBits);
while (len < WebpConstants.MaxAllowedCodeLength) while (len < WebpConstants.MaxAllowedCodeLength)

16
src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs

@ -57,11 +57,13 @@ internal static unsafe class PredictorEncoder
Span<short> scratch = stackalloc short[8]; Span<short> scratch = stackalloc short[8];
// TODO: Can we optimize this? // TODO: Can we optimize this?
int[][] histo = new int[4][]; int[][] histo =
for (int i = 0; i < 4; i++)
{ {
histo[i] = new int[256]; new int[256],
} new int[256],
new int[256],
new int[256]
};
if (lowEffort) if (lowEffort)
{ {
@ -233,7 +235,7 @@ internal static unsafe class PredictorEncoder
Span<byte> maxDiffs = MemoryMarshal.Cast<uint, byte>(currentRow[(width + 1)..]); Span<byte> maxDiffs = MemoryMarshal.Cast<uint, byte>(currentRow[(width + 1)..]);
float bestDiff = MaxDiffCost; float bestDiff = MaxDiffCost;
int bestMode = 0; int bestMode = 0;
uint[] residuals = new uint[1 << WebpConstants.MaxTransformBits]; Span<uint> residuals = stackalloc uint[1 << WebpConstants.MaxTransformBits]; // 256 bytes
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
histoArgb[i].AsSpan().Clear(); histoArgb[i].AsSpan().Clear();
@ -299,9 +301,7 @@ internal static unsafe class PredictorEncoder
if (curDiff < bestDiff) if (curDiff < bestDiff)
{ {
int[][] tmp = histoArgb; (bestHisto, histoArgb) = (histoArgb, bestHisto);
histoArgb = bestHisto;
bestHisto = tmp;
bestDiff = curDiff; bestDiff = curDiff;
bestMode = mode; bestMode = mode;
} }

58
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -23,7 +23,7 @@ internal class Vp8LEncoder : IDisposable
/// <summary> /// <summary>
/// Scratch buffer to reduce allocations. /// Scratch buffer to reduce allocations.
/// </summary> /// </summary>
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] }; private readonly int[][] histoArgb = { new int[256], new int[256], new int[256], new int[256] };
@ -549,12 +549,8 @@ internal class Vp8LEncoder : IDisposable
// bgra data with transformations applied. // bgra data with transformations applied.
Span<uint> bgra = this.EncodedData.GetSpan(); Span<uint> bgra = this.EncodedData.GetSpan();
int histogramImageXySize = LosslessUtils.SubSampleSize(width, this.HistoBits) * LosslessUtils.SubSampleSize(height, this.HistoBits); int histogramImageXySize = LosslessUtils.SubSampleSize(width, this.HistoBits) * LosslessUtils.SubSampleSize(height, this.HistoBits);
ushort[] histogramSymbols = new ushort[histogramImageXySize]; Span<ushort> histogramSymbols = histogramImageXySize <= 64 ? stackalloc ushort[histogramImageXySize] : new ushort[histogramImageXySize];
HuffmanTree[] huffTree = new HuffmanTree[3 * WebpConstants.CodeLengthCodes]; Span<HuffmanTree> huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes];
for (int i = 0; i < huffTree.Length; i++)
{
huffTree[i] = default;
}
if (useCache) if (useCache)
{ {
@ -607,10 +603,6 @@ internal class Vp8LEncoder : IDisposable
int histogramImageSize = histogramImage.Count; int histogramImageSize = histogramImage.Count;
int bitArraySize = 5 * histogramImageSize; int bitArraySize = 5 * histogramImageSize;
HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[bitArraySize]; HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[bitArraySize];
for (int i = 0; i < huffmanCodes.Length; i++)
{
huffmanCodes[i] = default;
}
GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
@ -702,7 +694,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary> /// </summary>
private void EncodePalette(bool lowEffort) private void EncodePalette(bool lowEffort)
{ {
Span<uint> tmpPalette = new uint[WebpConstants.MaxPaletteSize]; Span<uint> tmpPalette = stackalloc uint[WebpConstants.MaxPaletteSize];
int paletteSize = this.PaletteSize; int paletteSize = this.PaletteSize;
Span<uint> palette = this.Palette.Memory.Span; Span<uint> palette = this.Palette.Memory.Span;
this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); this.bitWriter.PutBits(WebpConstants.TransformPresent, 1);
@ -763,7 +755,7 @@ internal class Vp8LEncoder : IDisposable
int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits);
int transformHeight = LosslessUtils.SubSampleSize(height, 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(WebpConstants.TransformPresent, 1);
this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2);
@ -778,16 +770,7 @@ internal class Vp8LEncoder : IDisposable
ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol. ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol.
HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[5]; HuffmanTreeCode[] huffmanCodes = new HuffmanTreeCode[5];
for (int i = 0; i < huffmanCodes.Length; i++) Span<HuffmanTree> huffTree = stackalloc HuffmanTree[3 * WebpConstants.CodeLengthCodes];
{
huffmanCodes[i] = default;
}
HuffmanTree[] huffTree = new HuffmanTree[3UL * WebpConstants.CodeLengthCodes];
for (int i = 0; i < huffTree.Length; i++)
{
huffTree[i] = default;
}
// Calculate backward references from the image pixels. // Calculate backward references from the image pixels.
hashChain.Fill(bgra, quality, width, height, lowEffort); hashChain.Fill(bgra, quality, width, height, lowEffort);
@ -847,10 +830,10 @@ internal class Vp8LEncoder : IDisposable
this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes); this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes);
} }
private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) private void StoreHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{ {
int count = 0; int count = 0;
Span<int> symbols = this.scratch.AsSpan(0, 2); Span<int> symbols = this.scratch.Span.Slice(0, 2);
symbols.Clear(); symbols.Clear();
const int maxBits = 8; const int maxBits = 8;
const int maxSymbol = 1 << maxBits; const int maxSymbol = 1 << maxBits;
@ -901,7 +884,7 @@ internal class Vp8LEncoder : IDisposable
} }
} }
private void StoreFullHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) private void StoreFullHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
{ {
int i; int i;
byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes];
@ -1013,7 +996,7 @@ internal class Vp8LEncoder : IDisposable
} }
} }
private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, ushort[] histogramSymbols, HuffmanTreeCode[] huffmanCodes) private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span<ushort> histogramSymbols, HuffmanTreeCode[] huffmanCodes)
{ {
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1;
int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); int tileMask = histoBits == 0 ? 0 : -(1 << histoBits);
@ -1143,8 +1126,8 @@ internal class Vp8LEncoder : IDisposable
prevRow = currentRow; prevRow = currentRow;
} }
double[] entropyComp = new double[(int)HistoIx.HistoTotal]; Span<double> entropyComp = stackalloc double[(int)HistoIx.HistoTotal];
double[] entropy = new double[(int)EntropyIx.NumEntropyIx]; Span<double> entropy = stackalloc double[(int)EntropyIx.NumEntropyIx];
int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen; int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen;
// Let's add one zero to the predicted histograms. The zeros are removed // Let's add one zero to the predicted histograms. The zeros are removed
@ -1647,11 +1630,7 @@ internal class Vp8LEncoder : IDisposable
// Create Huffman trees. // Create Huffman trees.
bool[] bufRle = new bool[maxNumSymbols]; bool[] bufRle = new bool[maxNumSymbols];
HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols]; Span<HuffmanTree> huffTree = stackalloc HuffmanTree[3 * maxNumSymbols];
for (int i = 0; i < huffTree.Length; i++)
{
huffTree[i] = default;
}
for (int i = 0; i < histogramImage.Count; i++) for (int i = 0; i < histogramImage.Count; i++)
{ {
@ -1849,4 +1828,15 @@ internal class Vp8LEncoder : IDisposable
this.TransformData.Dispose(); this.TransformData.Dispose();
this.HashChain.Dispose(); this.HashChain.Dispose();
} }
/// <summary>
/// Scratch buffer to reduce allocations.
/// </summary>
private unsafe struct ScratchBuffer
{
private const int Size = 256;
private fixed int scratch[Size];
public Span<int> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
} }

5
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -498,10 +498,7 @@ internal sealed class WebpLosslessDecoder
private int ReadHuffmanCode(int alphabetSize, int[] codeLengths, Span<HuffmanCode> table) private int ReadHuffmanCode(int alphabetSize, int[] codeLengths, Span<HuffmanCode> table)
{ {
bool simpleCode = this.bitReader.ReadBit(); bool simpleCode = this.bitReader.ReadBit();
for (int i = 0; i < alphabetSize; i++) codeLengths.AsSpan(0, alphabetSize).Clear();
{
codeLengths[i] = 0;
}
if (simpleCode) if (simpleCode)
{ {

2
src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs

@ -121,7 +121,7 @@ internal static unsafe class QuantEnc
var rdi4 = new Vp8ModeScore(); var rdi4 = new Vp8ModeScore();
var rdTmp = new Vp8ModeScore(); var rdTmp = new Vp8ModeScore();
var res = new Vp8Residual(); var res = new Vp8Residual();
Span<short> tmpLevels = new short[16]; Span<short> tmpLevels = stackalloc short[16];
do do
{ {
const int numBlocks = 1; const int numBlocks = 1;

8
src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs

@ -374,7 +374,7 @@ internal class Vp8EncIterator
} }
else else
{ {
byte[] modes = new byte[16]; // DC4 Span<byte> modes = stackalloc byte[16]; // DC4
this.SetIntra4Mode(modes); this.SetIntra4Mode(modes);
} }
@ -407,7 +407,7 @@ internal class Vp8EncIterator
public int MbAnalyzeBestIntra4Mode(int bestAlpha) public int MbAnalyzeBestIntra4Mode(int bestAlpha)
{ {
byte[] modes = new byte[16]; Span<byte> modes = stackalloc byte[16];
const int maxMode = MaxIntra4Mode; const int maxMode = MaxIntra4Mode;
Vp8Histogram totalHisto = new(); Vp8Histogram totalHisto = new();
int curHisto = 0; int curHisto = 0;
@ -494,13 +494,13 @@ internal class Vp8EncIterator
this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16; this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16;
} }
public void SetIntra4Mode(byte[] modes) public void SetIntra4Mode(ReadOnlySpan<byte> modes)
{ {
int modesIdx = 0; int modesIdx = 0;
int predIdx = this.PredIdx; int predIdx = this.PredIdx;
for (int y = 4; y > 0; y--) 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; predIdx += this.predsWidth;
modesIdx += 4; modesIdx += 4;
} }

20
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -329,7 +329,7 @@ internal class Vp8Encoder : IDisposable
int uvStride = (yStride + 1) >> 1; int uvStride = (yStride + 1) >> 1;
Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); 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<int> alphas = stackalloc int[WebpConstants.MaxAlpha + 1];
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
int totalMb = this.Mbw * this.Mbw; int totalMb = this.Mbw * this.Mbw;
this.alpha /= totalMb; this.alpha /= totalMb;
@ -625,15 +625,15 @@ internal class Vp8Encoder : IDisposable
} }
// Simplified k-Means, to assign Nb segments based on alpha-histogram. // Simplified k-Means, to assign Nb segments based on alpha-histogram.
private void AssignSegments(int[] alphas) private void AssignSegments(ReadOnlySpan<int> alphas)
{ {
int nb = this.SegmentHeader.NumSegments < NumMbSegments ? this.SegmentHeader.NumSegments : NumMbSegments; int nb = this.SegmentHeader.NumSegments < NumMbSegments ? this.SegmentHeader.NumSegments : NumMbSegments;
int[] centers = new int[NumMbSegments]; Span<int> centers = stackalloc int[NumMbSegments];
int weightedAverage = 0; int weightedAverage = 0;
int[] map = new int[WebpConstants.MaxAlpha + 1]; Span<int> map = stackalloc int[WebpConstants.MaxAlpha + 1];
int n, k; int n, k;
int[] accum = new int[NumMbSegments]; Span<int> accum = stackalloc int[NumMbSegments];
int[] distAccum = new int[NumMbSegments]; Span<int> distAccum = stackalloc int[NumMbSegments];
// Bracket the input. // Bracket the input.
for (n = 0; n <= WebpConstants.MaxAlpha && alphas[n] == 0; ++n) for (n = 0; n <= WebpConstants.MaxAlpha && alphas[n] == 0; ++n)
@ -719,7 +719,7 @@ internal class Vp8Encoder : IDisposable
this.SetSegmentAlphas(centers, weightedAverage); this.SetSegmentAlphas(centers, weightedAverage);
} }
private void SetSegmentAlphas(int[] centers, int mid) private void SetSegmentAlphas(ReadOnlySpan<int> centers, int mid)
{ {
int nb = this.SegmentHeader.NumSegments; int nb = this.SegmentHeader.NumSegments;
Vp8SegmentInfo[] dqm = this.SegmentInfos; Vp8SegmentInfo[] dqm = this.SegmentInfos;
@ -840,7 +840,7 @@ internal class Vp8Encoder : IDisposable
private void SetSegmentProbas() private void SetSegmentProbas()
{ {
int[] p = new int[NumMbSegments]; Span<int> p = stackalloc int[NumMbSegments];
int n; int n;
for (n = 0; n < this.Mbw * this.Mbh; ++n) for (n = 0; n < this.Mbw * this.Mbh; ++n)
@ -931,7 +931,7 @@ internal class Vp8Encoder : IDisposable
} }
} }
private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span<byte> y, Span<byte> u, Span<byte> v, int yStride, int uvStride, int[] alphas, out int uvAlpha) private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span<byte> y, Span<byte> u, Span<byte> v, int yStride, int uvStride, Span<int> alphas, out int uvAlpha)
{ {
int alpha = 0; int alpha = 0;
uvAlpha = 0; uvAlpha = 0;
@ -952,7 +952,7 @@ internal class Vp8Encoder : IDisposable
return alpha; return alpha;
} }
private int MbAnalyze(Vp8EncIterator it, int[] alphas, out int bestUvAlpha) private int MbAnalyze(Vp8EncIterator it, Span<int> alphas, out int bestUvAlpha)
{ {
it.SetIntra16Mode(0); // default: Intra16, DC_PRED it.SetIntra16Mode(0); // default: Intra16, DC_PRED
it.SetSkip(false); // not skipped. it.SetSkip(false); // not skipped.

25
src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs

@ -10,12 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
internal sealed class Vp8Histogram 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];
/// <summary> /// <summary>
/// Size of histogram used by CollectHistogram. /// Size of histogram used by CollectHistogram.
/// </summary> /// </summary>
@ -47,17 +41,20 @@ internal sealed class Vp8Histogram
public void CollectHistogram(Span<byte> reference, Span<byte> pred, int startBlock, int endBlock) public void CollectHistogram(Span<byte> reference, Span<byte> pred, int startBlock, int endBlock)
{ {
Span<int> scratch = stackalloc int[16];
Span<short> output = stackalloc short[16];
Span<int> distribution = stackalloc int[MaxCoeffThresh + 1];
int j; int j;
this.distribution.AsSpan().Clear();
for (j = startBlock; j < endBlock; j++) 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. // Convert coefficients to bin.
if (Avx2.IsSupported) if (Avx2.IsSupported)
{ {
// Load. // Load.
ref short outputRef = ref MemoryMarshal.GetReference<short>(this.output); ref short outputRef = ref MemoryMarshal.GetReference(output);
Vector256<byte> out0 = Unsafe.As<short, Vector256<byte>>(ref outputRef); Vector256<byte> out0 = Unsafe.As<short, Vector256<byte>>(ref outputRef);
// v = abs(out) >> 3 // v = abs(out) >> 3
@ -73,21 +70,21 @@ internal sealed class Vp8Histogram
// Convert coefficients to bin. // Convert coefficients to bin.
for (int k = 0; k < 16; ++k) for (int k = 0; k < 16; ++k)
{ {
++this.distribution[this.output[k]]; ++distribution[output[k]];
} }
} }
else else
{ {
for (int k = 0; k < 16; ++k) 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); int clippedValue = ClipMax(v, MaxCoeffThresh);
++this.distribution[clippedValue]; ++distribution[clippedValue];
} }
} }
} }
this.SetHistogramData(this.distribution); this.SetHistogramData(distribution);
} }
public void Merge(Vp8Histogram other) public void Merge(Vp8Histogram other)
@ -103,7 +100,7 @@ internal sealed class Vp8Histogram
} }
} }
private void SetHistogramData(int[] distribution) private void SetHistogramData(ReadOnlySpan<int> distribution)
{ {
int maxValue = 0; int maxValue = 0;
int lastNonZero = 1; int lastNonZero = 1;

11
src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs

@ -15,10 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
/// </summary> /// </summary>
internal class Vp8Residual internal class Vp8Residual
{ {
private readonly byte[] scratch = new byte[32];
private readonly ushort[] scratchUShort = new ushort[16];
public int First { get; set; } public int First { get; set; }
public int Last { get; set; } public int Last { get; set; }
@ -162,9 +158,10 @@ internal class Vp8Residual
if (Avx2.IsSupported) if (Avx2.IsSupported)
{ {
Span<byte> ctxs = this.scratch.AsSpan(0, 16); Span<byte> scratch = stackalloc byte[32];
Span<byte> levels = this.scratch.AsSpan(16, 16); Span<byte> ctxs = scratch.Slice(0, 16);
Span<ushort> absLevels = this.scratchUShort.AsSpan(); Span<byte> levels = scratch.Slice(16);
Span<ushort> absLevels = stackalloc ushort[16];
// Precompute clamped levels and contexts, packed to 8b. // Precompute clamped levels and contexts, packed to 8b.
ref short outputRef = ref MemoryMarshal.GetReference<short>(this.Coeffs); ref short outputRef = ref MemoryMarshal.GetReference<short>(this.Coeffs);

27
src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs

@ -34,16 +34,6 @@ internal sealed class WebpLossyDecoder
/// </summary> /// </summary>
private readonly Configuration configuration; private readonly Configuration configuration;
/// <summary>
/// Scratch buffer to reduce allocations.
/// </summary>
private readonly int[] scratch = new int[16];
/// <summary>
/// Another scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] scratchBytes = new byte[4];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpLossyDecoder"/> class. /// Initializes a new instance of the <see cref="WebpLossyDecoder"/> class.
/// </summary> /// </summary>
@ -371,6 +361,9 @@ internal sealed class WebpLossyDecoder
} }
} }
Span<int> scratch = stackalloc int[16];
Span<byte> scratchBytes = stackalloc byte[4];
// Reconstruct one row. // Reconstruct one row.
for (int mbx = 0; mbx < dec.MbWidth; mbx++) for (int mbx = 0; mbx < dec.MbWidth; mbx++)
{ {
@ -448,7 +441,7 @@ internal sealed class WebpLossyDecoder
LossyUtils.TM4(dst, yuv, offset); LossyUtils.TM4(dst, yuv, offset);
break; break;
case 2: case 2:
LossyUtils.VE4(dst, yuv, offset, this.scratchBytes); LossyUtils.VE4(dst, yuv, offset, scratchBytes);
break; break;
case 3: case 3:
LossyUtils.HE4(dst, yuv, offset); LossyUtils.HE4(dst, yuv, offset);
@ -473,7 +466,7 @@ internal sealed class WebpLossyDecoder
break; break;
} }
DoTransform(bits, coeffs.AsSpan(n * 16), dst, this.scratch); DoTransform(bits, coeffs.AsSpan(n * 16), dst, scratch);
} }
} }
else else
@ -508,7 +501,7 @@ internal sealed class WebpLossyDecoder
{ {
for (int n = 0; n < 16; ++n, bits <<= 2) 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 @@ internal sealed class WebpLossyDecoder
break; break;
} }
DoUVTransform(bitsUv, coeffs.AsSpan(16 * 16), uDst, this.scratch); DoUVTransform(bitsUv, coeffs.AsSpan(16 * 16), uDst, scratch);
DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst, this.scratch); DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst, scratch);
// Stash away top samples for next block. // Stash away top samples for next block.
if (mby < dec.MbHeight - 1) if (mby < dec.MbHeight - 1)
@ -875,14 +868,14 @@ internal sealed class WebpLossyDecoder
else else
{ {
// Parse DC // Parse DC
short[] dc = new short[16]; Span<short> dc = stackalloc short[16];
int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs); int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs);
int nz = GetCoeffs(br, bands[1], ctx, q.Y2Mat, 0, dc); int nz = GetCoeffs(br, bands[1], ctx, q.Y2Mat, 0, dc);
mb.NoneZeroDcCoeffs = leftMb.NoneZeroDcCoeffs = (uint)(nz > 0 ? 1 : 0); mb.NoneZeroDcCoeffs = leftMb.NoneZeroDcCoeffs = (uint)(nz > 0 ? 1 : 0);
if (nz > 1) if (nz > 1)
{ {
// More than just the DC -> perform the full transform. // More than just the DC -> perform the full transform.
LossyUtils.TransformWht(dc, dst, this.scratch); LossyUtils.TransformWht(dc, dst, stackalloc int[16]);
} }
else else
{ {

35
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -17,11 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// </summary> /// </summary>
internal class WebpAnimationDecoder : IDisposable internal class WebpAnimationDecoder : IDisposable
{ {
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary> /// <summary>
/// Used for allocating memory during the decoding operations. /// Used for allocating memory during the decoding operations.
/// </summary> /// </summary>
@ -89,11 +84,12 @@ internal class WebpAnimationDecoder : IDisposable
this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata = this.metadata.GetWebpMetadata();
this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount; this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount;
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0; uint frameCount = 0;
int remainingBytes = (int)completeDataSize; int remainingBytes = (int)completeDataSize;
while (remainingBytes > 0) while (remainingBytes > 0)
{ {
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
remainingBytes -= 4; remainingBytes -= 4;
switch (chunkType) switch (chunkType)
{ {
@ -103,7 +99,7 @@ internal class WebpAnimationDecoder : IDisposable
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, this.buffer); WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
break; break;
default: default:
WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data"); WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
@ -134,15 +130,16 @@ internal class WebpAnimationDecoder : IDisposable
{ {
AnimationFrameData frameData = this.ReadFrameHeader(stream); AnimationFrameData frameData = this.ReadFrameHeader(stream);
long streamStartPosition = stream.Position; long streamStartPosition = stream.Position;
Span<byte> buffer = stackalloc byte[4];
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
bool hasAlpha = false; bool hasAlpha = false;
byte alphaChunkHeader = 0; byte alphaChunkHeader = 0;
if (chunkType is WebpChunkType.Alpha) if (chunkType is WebpChunkType.Alpha)
{ {
alphaChunkHeader = this.ReadAlphaData(stream); alphaChunkHeader = this.ReadAlphaData(stream);
hasAlpha = true; hasAlpha = true;
chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
} }
WebpImageInfo? webpInfo = null; WebpImageInfo? webpInfo = null;
@ -150,12 +147,12 @@ internal class WebpAnimationDecoder : IDisposable
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Vp8: case WebpChunkType.Vp8:
webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features);
features.Alpha = hasAlpha; features.Alpha = hasAlpha;
features.AlphaChunkHeader = alphaChunkHeader; features.AlphaChunkHeader = alphaChunkHeader;
break; break;
case WebpChunkType.Vp8L: case WebpChunkType.Vp8L:
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
break; break;
default: default:
WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L"); WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L");
@ -226,7 +223,7 @@ internal class WebpAnimationDecoder : IDisposable
{ {
this.alphaData?.Dispose(); this.alphaData?.Dispose();
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, stackalloc byte[4]);
int alphaDataSize = (int)(alphaChunkSize - 1); int alphaDataSize = (int)(alphaChunkSize - 1);
this.alphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize); this.alphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
@ -353,24 +350,26 @@ internal class WebpAnimationDecoder : IDisposable
/// <returns>Animation frame data.</returns> /// <returns>Animation frame data.</returns>
private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) private AnimationFrameData ReadFrameHeader(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[4];
AnimationFrameData data = new() 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. // 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. // 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. // Frame width Minus One.
Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1, Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
// Frame height Minus One. // Frame height Minus One.
Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1, Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
// Frame duration. // Frame duration.
Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer)
}; };
byte flags = (byte)stream.ReadByte(); byte flags = (byte)stream.ReadByte();

26
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -18,7 +18,7 @@ internal static class WebpChunkParsingUtils
/// Reads the header of a lossy webp image. /// Reads the header of a lossy webp image.
/// </summary> /// </summary>
/// <returns>Information about this webp image.</returns> /// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
// VP8 data size (not including this 4 bytes). // VP8 data size (not including this 4 bytes).
int bytesRead = stream.Read(buffer, 0, 4); int bytesRead = stream.Read(buffer, 0, 4);
@ -77,7 +77,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); 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"); WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
} }
@ -91,7 +91,7 @@ internal static class WebpChunkParsingUtils
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
uint width = tmp & 0x3fff; uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6); sbyte xScale = (sbyte)(tmp >> 6);
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2)); tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2));
uint height = tmp & 0x3fff; uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6); sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7; remaining -= 7;
@ -140,7 +140,7 @@ internal static class WebpChunkParsingUtils
/// Reads the header of a lossless webp image. /// Reads the header of a lossless webp image.
/// </summary> /// </summary>
/// <returns>Information about this image.</returns> /// <returns>Information about this image.</returns>
public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
// VP8 data size. // VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer); uint imageDataSize = ReadChunkSize(stream, buffer);
@ -195,7 +195,7 @@ internal static class WebpChunkParsingUtils
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
/// </summary> /// </summary>
/// <returns>Information about this webp image.</returns> /// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buffer, WebpFeatures features) public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span<byte> buffer, WebpFeatures features)
{ {
uint fileSize = ReadChunkSize(stream, buffer); uint fileSize = ReadChunkSize(stream, buffer);
@ -253,7 +253,7 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param> /// <param name="buffer">The buffer to store the read data into.</param>
/// <returns>A unsigned 24 bit integer.</returns> /// <returns>A unsigned 24 bit integer.</returns>
public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer) public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span<byte> buffer)
{ {
if (stream.Read(buffer, 0, 3) == 3) if (stream.Read(buffer, 0, 3) == 3)
{ {
@ -271,9 +271,11 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read the data from.</param> /// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param> /// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer) public static uint ReadChunkSize(BufferedReadStream stream, Span<byte> 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); uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
@ -290,9 +292,11 @@ internal static class WebpChunkParsingUtils
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid. /// Thrown if the input stream is not valid.
/// </exception> /// </exception>
public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buffer) public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> 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); var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType; return chunkType;
@ -306,7 +310,7 @@ internal static class WebpChunkParsingUtils
/// If there are more such chunks, readers MAY ignore all except the first one. /// 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. /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// </summary> /// </summary>
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<byte> buffer)
{ {
long streamLength = stream.Length; long streamLength = stream.Length;
while (stream.Position < streamLength) while (stream.Position < streamLength)

104
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -18,11 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// </summary> /// </summary>
internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{ {
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary> /// <summary>
/// General configuration options. /// General configuration options.
/// </summary> /// </summary>
@ -80,8 +75,9 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
try try
{ {
ImageMetadata metadata = new(); ImageMetadata metadata = new();
Span<byte> buffer = stackalloc byte[4];
uint fileSize = this.ReadImageHeader(stream); uint fileSize = ReadImageHeader(stream, buffer);
using (this.webImageInfo = this.ReadVp8Info(stream, metadata)) using (this.webImageInfo = this.ReadVp8Info(stream, metadata))
{ {
@ -112,7 +108,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
// There can be optional chunks after the image data, like EXIF and XMP. // There can be optional chunks after the image data, like EXIF and XMP.
if (this.webImageInfo.Features != null) if (this.webImageInfo.Features != null)
{ {
this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features); this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features, buffer);
} }
return image; return image;
@ -128,7 +124,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ReadImageHeader(stream); ReadImageHeader(stream, stackalloc byte[4]);
ImageMetadata metadata = new(); ImageMetadata metadata = new();
using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true))
@ -144,8 +140,9 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// Reads and skips over the image header. /// Reads and skips over the image header.
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <returns>The file size in bytes.</returns> /// <returns>The file size in bytes.</returns>
private uint ReadImageHeader(BufferedReadStream stream) private static uint ReadImageHeader(BufferedReadStream stream, Span<byte> buffer)
{ {
// Skip FourCC header, we already know its a RIFF file at this point. // Skip FourCC header, we already know its a RIFF file at this point.
stream.Skip(4); stream.Skip(4);
@ -153,7 +150,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
// Read file size. // Read file size.
// The size of the file in bytes starting at offset 8. // 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. // 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. // Skip 'WEBP' from the header.
stream.Skip(4); stream.Skip(4);
@ -172,35 +169,36 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{ {
WebpMetadata webpMetadata = metadata.GetFormatMetadata(WebpFormat.Instance); WebpMetadata webpMetadata = metadata.GetFormatMetadata(WebpFormat.Instance);
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); Span<byte> buffer = stackalloc byte[4];
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
WebpFeatures features = new(); WebpFeatures features = new();
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Vp8: case WebpChunkType.Vp8:
webpMetadata.FileFormat = WebpFileFormatType.Lossy; webpMetadata.FileFormat = WebpFileFormatType.Lossy;
return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features);
case WebpChunkType.Vp8L: case WebpChunkType.Vp8L:
webpMetadata.FileFormat = WebpFileFormatType.Lossless; webpMetadata.FileFormat = WebpFileFormatType.Lossless;
return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
case WebpChunkType.Vp8X: case WebpChunkType.Vp8X:
WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, this.buffer, features); WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features);
while (stream.Position < stream.Length) while (stream.Position < stream.Length)
{ {
chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Vp8) if (chunkType == WebpChunkType.Vp8)
{ {
webpMetadata.FileFormat = WebpFileFormatType.Lossy; 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) else if (chunkType == WebpChunkType.Vp8L)
{ {
webpMetadata.FileFormat = WebpFileFormatType.Lossless; 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)) 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) if (isAnimationChunk)
{ {
return webpInfos; return webpInfos;
@ -209,7 +207,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
else else
{ {
// Ignore unknown chunks. // Ignore unknown chunks.
uint chunkSize = this.ReadChunkSize(stream); uint chunkSize = ReadChunkSize(stream, buffer);
stream.Skip((int)chunkSize); stream.Skip((int)chunkSize);
} }
} }
@ -230,34 +228,36 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <param name="chunkType">The chunk type.</param> /// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param> /// <param name="features">The webp image features.</param>
/// <param name="ignoreAlpha">For identify, the alpha data should not be read.</param> /// <param name="ignoreAlpha">For identify, the alpha data should not be read.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <returns>true, if its a alpha chunk.</returns> /// <returns>true, if its a alpha chunk.</returns>
private bool ParseOptionalExtendedChunks( private bool ParseOptionalExtendedChunks(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
WebpChunkType chunkType, WebpChunkType chunkType,
WebpFeatures features, WebpFeatures features,
bool ignoreAlpha) bool ignoreAlpha,
Span<byte> buffer)
{ {
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
this.ReadIccProfile(stream, metadata); this.ReadIccProfile(stream, metadata, buffer);
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
this.ReadExifProfile(stream, metadata); this.ReadExifProfile(stream, metadata, buffer);
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
this.ReadXmpProfile(stream, metadata); this.ReadXmpProfile(stream, metadata, buffer);
break; break;
case WebpChunkType.AnimationParameter: case WebpChunkType.AnimationParameter:
this.ReadAnimationParameters(stream, features); ReadAnimationParameters(stream, features, buffer);
return true; return true;
case WebpChunkType.Alpha: case WebpChunkType.Alpha:
this.ReadAlphaData(stream, features, ignoreAlpha); this.ReadAlphaData(stream, features, ignoreAlpha, buffer);
break; break;
default: default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
@ -273,7 +273,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="features">The webp features.</param> /// <param name="features">The webp features.</param>
private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features) /// <param name="buffer">Temporary buffer.</param>
private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer)
{ {
if (this.skipMetadata || (!features.ExifProfile && !features.XmpMetaData)) if (this.skipMetadata || (!features.ExifProfile && !features.XmpMetaData))
{ {
@ -284,19 +285,19 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
while (stream.Position < streamLength) while (stream.Position < streamLength)
{ {
// Read chunk header. // Read chunk header.
WebpChunkType chunkType = this.ReadChunkType(stream); WebpChunkType chunkType = ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null)
{ {
this.ReadExifProfile(stream, metadata); this.ReadExifProfile(stream, metadata, buffer);
} }
else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null)
{ {
this.ReadXmpProfile(stream, metadata); this.ReadXmpProfile(stream, metadata, buffer);
} }
else else
{ {
// Skip duplicate XMP or EXIF chunk. // Skip duplicate XMP or EXIF chunk.
uint chunkLength = this.ReadChunkSize(stream); uint chunkLength = ReadChunkSize(stream, buffer);
stream.Skip((int)chunkLength); stream.Skip((int)chunkLength);
} }
} }
@ -307,9 +308,10 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata) /// <param name="buffer">Temporary buffer.</param>
private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{ {
uint exifChunkSize = this.ReadChunkSize(stream); uint exifChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata) if (this.skipMetadata)
{ {
stream.Skip((int)exifChunkSize); stream.Skip((int)exifChunkSize);
@ -333,9 +335,10 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata) /// <param name="buffer">Temporary buffer.</param>
private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{ {
uint xmpChunkSize = this.ReadChunkSize(stream); uint xmpChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata) if (this.skipMetadata)
{ {
stream.Skip((int)xmpChunkSize); stream.Skip((int)xmpChunkSize);
@ -359,9 +362,10 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata) /// <param name="buffer">Temporary buffer.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{ {
uint iccpChunkSize = this.ReadChunkSize(stream); uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata) if (this.skipMetadata)
{ {
stream.Skip((int)iccpChunkSize); stream.Skip((int)iccpChunkSize);
@ -388,22 +392,23 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="features">The webp features.</param> /// <param name="features">The webp features.</param>
private void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features) /// <param name="buffer">Temporary buffer.</param>
private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features, Span<byte> buffer)
{ {
features.Animation = true; features.Animation = true;
uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer);
byte blue = (byte)stream.ReadByte(); byte blue = (byte)stream.ReadByte();
byte green = (byte)stream.ReadByte(); byte green = (byte)stream.ReadByte();
byte red = (byte)stream.ReadByte(); byte red = (byte)stream.ReadByte();
byte alpha = (byte)stream.ReadByte(); byte alpha = (byte)stream.ReadByte();
features.AnimationBackgroundColor = new Color(new Rgba32(red, green, blue, alpha)); 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) if (bytesRead != 2)
{ {
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count"); WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count");
} }
features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(this.buffer); features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
} }
/// <summary> /// <summary>
@ -412,9 +417,10 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="features">The features.</param> /// <param name="features">The features.</param>
/// <param name="ignoreAlpha">if set to true, skips the chunk data.</param> /// <param name="ignoreAlpha">if set to true, skips the chunk data.</param>
private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha) /// <param name="buffer">Temporary buffer.</param>
private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha, Span<byte> buffer)
{ {
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer);
if (ignoreAlpha) if (ignoreAlpha)
{ {
stream.Skip((int)alphaChunkSize); stream.Skip((int)alphaChunkSize);
@ -436,14 +442,15 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// Identifies the chunk type from the chunk. /// Identifies the chunk type from the chunk.
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid. /// Thrown if the input stream is not valid.
/// </exception> /// </exception>
private WebpChunkType ReadChunkType(BufferedReadStream stream) private static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> 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."); throw new ImageFormatException("Invalid Webp data.");
@ -454,13 +461,14 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// so the chunk size will be increased by 1 in those cases. /// so the chunk size will be increased by 1 in those cases.
/// </summary> /// </summary>
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
/// <exception cref="ImageFormatException">Invalid data.</exception> /// <exception cref="ImageFormatException">Invalid data.</exception>
private uint ReadChunkSize(BufferedReadStream stream) private static uint ReadChunkSize(BufferedReadStream stream, Span<byte> 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; return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
} }

33
src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs

@ -86,10 +86,6 @@ internal class ExifReader : BaseExifReader
/// </summary> /// </summary>
internal abstract class BaseExifReader 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 MemoryAllocator? allocator;
private readonly Stream data; private readonly Stream data;
private List<ExifTag>? invalidTags; private List<ExifTag>? invalidTags;
@ -528,20 +524,33 @@ internal abstract class BaseExifReader
return read == length; return read == length;
} }
protected ulong ReadUInt64() => protected ulong ReadUInt64()
this.TryReadSpan(this.buf8) {
? this.ConvertToUInt64(this.buf8) Span<byte> buffer = stackalloc byte[8];
return this.TryReadSpan(buffer)
? this.ConvertToUInt64(buffer)
: default; : default;
}
// Known as Long in Exif Specification. // Known as Long in Exif Specification.
protected uint ReadUInt32() => protected uint ReadUInt32()
this.TryReadSpan(this.buf4) {
? this.ConvertToUInt32(this.buf4) Span<byte> buffer = stackalloc byte[4];
return this.TryReadSpan(buffer)
? this.ConvertToUInt32(buffer)
: default; : default;
}
protected ushort ReadUInt16() => this.TryReadSpan(this.buf2) protected ushort ReadUInt16()
? this.ConvertToShort(this.buf2) {
Span<byte> buffer = stackalloc byte[2];
return this.TryReadSpan(buffer)
? this.ConvertToShort(buffer)
: default; : default;
}
private long ConvertToInt64(ReadOnlySpan<byte> buffer) private long ConvertToInt64(ReadOnlySpan<byte> buffer)
{ {

4
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifByteArray.cs

@ -45,12 +45,12 @@ internal sealed class ExifByteArray : ExifArrayValue<byte>
private bool TrySetSignedIntArray(int[] intArrayValue) 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; return false;
} }
var value = new byte[intArrayValue.Length]; byte[] value = new byte[intArrayValue.Length];
for (int i = 0; i < intArrayValue.Length; i++) for (int i = 0; i < intArrayValue.Length; i++)
{ {
int s = intArrayValue[i]; int s = intArrayValue[i];

14
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -108,10 +108,10 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
const int profileIdPos = 84; const int profileIdPos = 84;
// need to copy some values because they need to be zero for the hashing // need to copy some values because they need to be zero for the hashing
byte[] temp = new byte[24]; Span<byte> temp = stackalloc byte[24];
Buffer.BlockCopy(data, profileFlagPos, temp, 0, 4); data.AsSpan(profileFlagPos, 4).CopyTo(temp);
Buffer.BlockCopy(data, renderingIntentPos, temp, 4, 4); data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4));
Buffer.BlockCopy(data, profileIdPos, temp, 8, 16); data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8));
try try
{ {
@ -131,9 +131,9 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
} }
finally finally
{ {
Buffer.BlockCopy(temp, 0, data, profileFlagPos, 4); temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos));
Buffer.BlockCopy(temp, 4, data, renderingIntentPos, 4); temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos));
Buffer.BlockCopy(temp, 8, data, profileIdPos, 16); temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos));
} }
} }

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs

@ -37,4 +37,12 @@ public class YCbCrColorConversion : ColorConversionBenchmark
new JpegColorConverterBase.YCbCrAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YCbCrAvx(8).ConvertToRgbInplace(values);
} }
[Benchmark]
public void SimdVectorArm()
{
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.YCbCrArm(8).ConvertToRgbInplace(values);
}
} }

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs

@ -37,4 +37,12 @@ public class YccKColorConverter : ColorConversionBenchmark
new JpegColorConverterBase.YccKAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YccKAvx(8).ConvertToRgbInplace(values);
} }
[Benchmark]
public void SimdVectorArm64()
{
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.YccKArm64(8).ConvertToRgbInplace(values);
}
} }

237
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.ColorSpaces; using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
@ -69,6 +71,171 @@ public class JpegColorConverterTests
Assert.Equal(precision, converter.Precision); Assert.Equal(precision, converter.Precision);
} }
[Fact]
public void GetConverterReturnsCorrectConverterWithRgbColorSpace()
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
// arrange
Type expectedType = typeof(JpegColorConverterBase.RgbScalar);
if (Avx.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.RgbAvx);
}
else if (Sse2.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.RgbVector);
}
else if (AdvSimd.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.RgbArm);
}
// act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8);
Type actualType = converter.GetType();
// assert
Assert.Equal(expectedType, actualType);
}
}
[Fact]
public void GetConverterReturnsCorrectConverterWithGrayScaleColorSpace()
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
// arrange
Type expectedType = typeof(JpegColorConverterBase.GrayscaleScalar);
if (Avx.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.GrayscaleAvx);
}
else if (Sse2.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.GrayScaleVector);
}
else if (AdvSimd.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.GrayscaleArm);
}
// act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8);
Type actualType = converter.GetType();
// assert
Assert.Equal(expectedType, actualType);
}
}
[Fact]
public void GetConverterReturnsCorrectConverterWithCmykColorSpace()
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
// arrange
Type expectedType = typeof(JpegColorConverterBase.CmykScalar);
if (Avx.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.CmykAvx);
}
else if (Sse2.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.CmykVector);
}
else if (AdvSimd.Arm64.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.CmykArm64);
}
// act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8);
Type actualType = converter.GetType();
// assert
Assert.Equal(expectedType, actualType);
}
}
[Fact]
public void GetConverterReturnsCorrectConverterWithYCbCrColorSpace()
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
// arrange
Type expectedType = typeof(JpegColorConverterBase.YCbCrScalar);
if (Avx.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YCbCrAvx);
}
else if (Sse2.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YCbCrVector);
}
else if (AdvSimd.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YCbCrArm);
}
// act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8);
Type actualType = converter.GetType();
// assert
Assert.Equal(expectedType, actualType);
}
}
[Fact]
public void GetConverterReturnsCorrectConverterWithYcckColorSpace()
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
// arrange
Type expectedType = typeof(JpegColorConverterBase.YccKScalar);
if (Avx.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YccKAvx);
}
else if (Sse2.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YccKVector);
}
else if (AdvSimd.Arm64.IsSupported)
{
expectedType = typeof(JpegColorConverterBase.YccKArm64);
}
// act
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8);
Type actualType = converter.GetType();
// assert
Assert.Equal(expectedType, actualType);
}
}
[Theory] [Theory]
[InlineData(JpegColorSpace.Grayscale, 1)] [InlineData(JpegColorSpace.Grayscale, 1)]
[InlineData(JpegColorSpace.Ycck, 4)] [InlineData(JpegColorSpace.Ycck, 4)]
@ -242,7 +409,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYCbCrAvx2(int seed) => public void FromYCbCrAvx2(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.YCbCrAvx(8), this.TestConversionToRgb(
new JpegColorConverterBase.YCbCrAvx(8),
3, 3,
seed, seed,
new JpegColorConverterBase.YCbCrScalar(8)); new JpegColorConverterBase.YCbCrScalar(8));
@ -250,7 +418,25 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbToYCbCrAvx2(int seed) => public void FromRgbToYCbCrAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YCbCrAvx(8), this.TestConversionFromRgb(
new JpegColorConverterBase.YCbCrAvx(8),
3,
seed,
new JpegColorConverterBase.YCbCrScalar(8),
precísion: 2);
[Theory]
[MemberData(nameof(Seeds))]
public void FromYCbCrArm(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.YCbCrArm(8),
3,
seed,
new JpegColorConverterBase.YCbCrScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToYCbCrArm(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YCbCrArm(8),
3, 3,
seed, seed,
new JpegColorConverterBase.YCbCrScalar(8), new JpegColorConverterBase.YCbCrScalar(8),
@ -259,7 +445,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykAvx2(int seed) => public void FromCmykAvx2(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.CmykAvx(8), this.TestConversionToRgb(
new JpegColorConverterBase.CmykAvx(8),
4, 4,
seed, seed,
new JpegColorConverterBase.CmykScalar(8)); new JpegColorConverterBase.CmykScalar(8));
@ -267,7 +454,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbToCmykAvx2(int seed) => public void FromRgbToCmykAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.CmykAvx(8), this.TestConversionFromRgb(
new JpegColorConverterBase.CmykAvx(8),
4, 4,
seed, seed,
new JpegColorConverterBase.CmykScalar(8), new JpegColorConverterBase.CmykScalar(8),
@ -276,7 +464,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykArm(int seed) => public void FromCmykArm(int seed) =>
this.TestConversionToRgb( new JpegColorConverterBase.CmykArm64(8), this.TestConversionToRgb(
new JpegColorConverterBase.CmykArm64(8),
4, 4,
seed, seed,
new JpegColorConverterBase.CmykScalar(8)); new JpegColorConverterBase.CmykScalar(8));
@ -284,7 +473,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbToCmykArm(int seed) => public void FromRgbToCmykArm(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.CmykArm64(8), this.TestConversionFromRgb(
new JpegColorConverterBase.CmykArm64(8),
4, 4,
seed, seed,
new JpegColorConverterBase.CmykScalar(8), new JpegColorConverterBase.CmykScalar(8),
@ -293,7 +483,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromGrayscaleAvx2(int seed) => public void FromGrayscaleAvx2(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.GrayscaleAvx(8), this.TestConversionToRgb(
new JpegColorConverterBase.GrayscaleAvx(8),
1, 1,
seed, seed,
new JpegColorConverterBase.GrayscaleScalar(8)); new JpegColorConverterBase.GrayscaleScalar(8));
@ -301,7 +492,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbToGrayscaleAvx2(int seed) => public void FromRgbToGrayscaleAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.GrayscaleAvx(8), this.TestConversionFromRgb(
new JpegColorConverterBase.GrayscaleAvx(8),
1, 1,
seed, seed,
new JpegColorConverterBase.GrayscaleScalar(8), new JpegColorConverterBase.GrayscaleScalar(8),
@ -327,7 +519,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbAvx2(int seed) => public void FromRgbAvx2(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.RgbAvx(8), this.TestConversionToRgb(
new JpegColorConverterBase.RgbAvx(8),
3, 3,
seed, seed,
new JpegColorConverterBase.RgbScalar(8)); new JpegColorConverterBase.RgbScalar(8));
@ -335,7 +528,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbArm(int seed) => public void FromRgbArm(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.RgbArm(8), this.TestConversionToRgb(
new JpegColorConverterBase.RgbArm(8),
3, 3,
seed, seed,
new JpegColorConverterBase.RgbScalar(8)); new JpegColorConverterBase.RgbScalar(8));
@ -343,7 +537,8 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYccKAvx2(int seed) => public void FromYccKAvx2(int seed) =>
this.TestConversionToRgb( new JpegColorConverterBase.YccKAvx(8), this.TestConversionToRgb(
new JpegColorConverterBase.YccKAvx(8),
4, 4,
seed, seed,
new JpegColorConverterBase.YccKScalar(8)); new JpegColorConverterBase.YccKScalar(8));
@ -351,7 +546,25 @@ public class JpegColorConverterTests
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbToYccKAvx2(int seed) => public void FromRgbToYccKAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YccKAvx(8), this.TestConversionFromRgb(
new JpegColorConverterBase.YccKAvx(8),
4,
seed,
new JpegColorConverterBase.YccKScalar(8),
precísion: 4);
[Theory]
[MemberData(nameof(Seeds))]
public void FromYccKArm64(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.YccKArm64(8),
4,
seed,
new JpegColorConverterBase.YccKScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToYccKArm64(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YccKArm64(8),
4, 4,
seed, seed,
new JpegColorConverterBase.YccKScalar(8), new JpegColorConverterBase.YccKScalar(8),

4
tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs

@ -211,8 +211,8 @@ public class BigTiffMetadataTests
foreach (IExifValue entry in values) foreach (IExifValue entry in values)
{ {
writer.Write((ushort)entry.Tag); writer.Write((ushort)entry.Tag, buffer);
writer.Write((ushort)entry.DataType); writer.Write((ushort)entry.DataType, buffer);
WriteLong8(writer, buffer, ExifWriter.GetNumberOfComponents(entry)); WriteLong8(writer, buffer, ExifWriter.GetNumberOfComponents(entry));
uint length = ExifWriter.GetLength(entry); uint length = ExifWriter.GetLength(entry);

4
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs

@ -19,7 +19,7 @@ public class TiffEncoderHeaderTests
using (TiffStreamWriter writer = new(stream)) 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()); Assert.Equal(new byte[] { 0x49, 0x49, 42, 0, 0x00, 0x00, 0x00, 0x00 }, stream.ToArray());
@ -32,7 +32,7 @@ public class TiffEncoderHeaderTests
TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator); TiffEncoderCore encoder = new(Encoder, Configuration.Default.MemoryAllocator);
using TiffStreamWriter writer = new(stream); using TiffStreamWriter writer = new(stream);
long firstIfdMarker = TiffEncoderCore.WriteHeader(writer); long firstIfdMarker = TiffEncoderCore.WriteHeader(writer, stackalloc byte[4]);
Assert.Equal(4, firstIfdMarker); Assert.Equal(4, firstIfdMarker);
} }
} }

15
tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs

@ -53,7 +53,7 @@ public class TiffWriterTests
{ {
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new TiffStreamWriter(stream); using var writer = new TiffStreamWriter(stream);
writer.Write(1234); writer.Write(1234, stackalloc byte[2]);
Assert.Equal(new byte[] { 0xD2, 0x04 }, stream.ToArray()); Assert.Equal(new byte[] { 0xD2, 0x04 }, stream.ToArray());
} }
@ -63,7 +63,7 @@ public class TiffWriterTests
{ {
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new TiffStreamWriter(stream); 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()); Assert.Equal(new byte[] { 0x4E, 0x61, 0xBC, 0x00 }, stream.ToArray());
} }
@ -89,16 +89,17 @@ public class TiffWriterTests
public void WriteMarker_WritesToPlacedPosition() public void WriteMarker_WritesToPlacedPosition()
{ {
using var stream = new MemoryStream(); using var stream = new MemoryStream();
Span<byte> buffer = stackalloc byte[4];
using (var writer = new TiffStreamWriter(stream)) using (var writer = new TiffStreamWriter(stream))
{ {
writer.Write(0x11111111); writer.Write(0x11111111, buffer);
long marker = writer.PlaceMarker(); long marker = writer.PlaceMarker(buffer);
writer.Write(0x33333333); writer.Write(0x33333333, buffer);
writer.WriteMarker(marker, 0x12345678); writer.WriteMarker(marker, 0x12345678, buffer);
writer.Write(0x44444444); writer.Write(0x44444444, buffer);
} }
Assert.Equal( Assert.Equal(

Loading…
Cancel
Save