Browse Source

Add row-stride overloads for memory APIs

pull/3066/head
James Jackson-South 2 months ago
parent
commit
17699d1953
  1. 120
      src/ImageSharp/Image.LoadPixelData.cs
  2. 534
      src/ImageSharp/Image.WrapMemory.cs
  3. 63
      src/ImageSharp/ImageFrame.LoadPixelData.cs
  4. 2
      src/ImageSharp/ImageFrame.cs
  5. 9
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  6. 99
      src/ImageSharp/ImageFrame{TPixel}.cs
  7. 53
      src/ImageSharp/Image{TPixel}.cs
  8. 6
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  9. 12
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  10. 229
      src/ImageSharp/Memory/Buffer2D{T}.cs
  11. 191
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  12. 6
      src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
  13. 8
      src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs
  14. 6
      src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs
  15. 2
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs
  16. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  17. 2
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  18. 2
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
  19. 74
      tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
  20. 97
      tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs
  21. 43
      tests/ImageSharp.Tests/Memory/Buffer2DTests.CopyTo.cs
  22. 69
      tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs
  23. 91
      tests/ImageSharp.Tests/Memory/Buffer2DTests.WrapMemory.cs
  24. 46
      tests/ImageSharp.Tests/Memory/Buffer2DTests.cs
  25. 126
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs
  26. 78
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

120
src/ImageSharp/Image.LoadPixelData.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
@ -25,6 +24,27 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(Configuration.Default, data, width, height);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from raw <typeparamref name="TPixel"/> data
/// using <paramref name="rowStride"/> pixels between source row starts.
/// </summary>
/// <param name="data">The readonly span containing image data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStride">The number of pixels between row starts in <paramref name="data"/>.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStride"/> is less than <paramref name="width"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="data"/> is smaller than <c>((height - 1) * rowStride) + width</c>.
/// </exception>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> LoadPixelData<TPixel>(ReadOnlySpan<TPixel> data, int width, int height, int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(Configuration.Default, data, width, height, rowStride);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the given readonly span of bytes in <typeparamref name="TPixel"/> format.
/// </summary>
@ -38,6 +58,28 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData<TPixel>(Configuration.Default, data, width, height);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from a readonly span of bytes in
/// <typeparamref name="TPixel"/> format using <paramref name="rowStrideInBytes"/> bytes between source row starts.
/// </summary>
/// <param name="data">The readonly span containing image data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in <paramref name="data"/>.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStrideInBytes"/> resolves to fewer than <paramref name="width"/> pixels.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="rowStrideInBytes"/> is not divisible by the pixel size,
/// or <paramref name="data"/> is smaller than the required strided image length.
/// </exception>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> LoadPixelData<TPixel>(ReadOnlySpan<byte> data, int width, int height, int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData<TPixel>(Configuration.Default, data, width, height, rowStrideInBytes);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the given readonly span of bytes in <typeparamref name="TPixel"/> format.
/// </summary>
@ -53,6 +95,40 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(configuration, MemoryMarshal.Cast<byte, TPixel>(data), width, height);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from a readonly span of bytes in
/// <typeparamref name="TPixel"/> format using <paramref name="rowStrideInBytes"/> bytes between source row starts.
/// </summary>
/// <param name="configuration">The configuration for the decoder.</param>
/// <param name="data">The readonly span containing image data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in <paramref name="data"/>.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStrideInBytes"/> resolves to fewer than <paramref name="width"/> pixels.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="rowStrideInBytes"/> is not divisible by the pixel size,
/// or <paramref name="data"/> is smaller than the required strided image length.
/// </exception>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> LoadPixelData<TPixel>(
Configuration configuration,
ReadOnlySpan<byte> data,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
int rowStride = GetPixelRowStrideFromByteStride<TPixel>(width, rowStrideInBytes, nameof(rowStrideInBytes));
return LoadPixelData(configuration, MemoryMarshal.Cast<byte, TPixel>(data), width, height, rowStride);
}
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the raw <typeparamref name="TPixel"/> data.
/// </summary>
@ -66,20 +142,42 @@ public abstract partial class Image
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> LoadPixelData<TPixel>(Configuration configuration, ReadOnlySpan<TPixel> data, int width, int height)
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(configuration, data, width, height, width);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from raw <typeparamref name="TPixel"/> data
/// using <paramref name="rowStride"/> pixels between source row starts.
/// </summary>
/// <param name="configuration">The configuration for the decoder.</param>
/// <param name="data">The readonly span containing the image pixel data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStride">The number of pixels between row starts in <paramref name="data"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStride"/> is less than <paramref name="width"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="data"/> is smaller than <c>((height - 1) * rowStride) + width</c>.
/// </exception>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> LoadPixelData<TPixel>(
Configuration configuration,
ReadOnlySpan<TPixel> data,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
if (data.IsEmpty)
{
throw new ArgumentException("Pixel data cannot be empty.", nameof(data));
}
int count = width * height;
Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data));
ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride));
long requiredLength = GetRequiredLength(width, height, rowStride);
Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data));
Image<TPixel> image = new(configuration, width, height);
data = data[..count];
data.CopyTo(image.Frames.RootFrame.PixelBuffer.FastMemoryGroup);
image.Frames.RootFrame.PixelBuffer.CopyFrom(data, rowStride);
return image;
}

534
src/ImageSharp/Image.WrapMemory.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -58,29 +59,55 @@ public abstract partial class Image
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input <see cref="Memory{T}"/>
/// to the new <see cref="Image{TPixel}"/> instance. This means that consumers of this method must ensure that the input buffer
/// is either self-contained, (for example, a <see cref="Memory{T}"/> instance wrapping a new array that was
/// created), or that the owning object is not disposed until the returned <see cref="Image{TPixel}"/> is disposed.
/// Wraps an existing memory area allowing viewing/manipulation as an <see cref="Image{TPixel}"/>
/// with <paramref name="rowStride"/> pixels between row starts.
/// </para>
/// <para>
/// If the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately.
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input
/// <see cref="Memory{T}"/> to the new <see cref="Image{TPixel}"/> instance. Consumers must ensure that
/// the input buffer remains valid for the full lifetime of the returned image.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="pixelMemory">The pixel memory.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <param name="pixelMemory">The source pixel memory.</param>
/// <param name="width">The width of the memory image in pixels.</param>
/// <param name="height">The height of the memory image in pixels.</param>
/// <param name="rowStride">The number of pixels between row starts in <paramref name="pixelMemory"/>.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStride"/> is less than <paramref name="width"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// The length of <paramref name="pixelMemory"/> is less than
/// <c>((height - 1) * rowStride) + width</c>.
/// </exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<TPixel> pixelMemory,
int width,
int height,
int rowStride,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride));
long requiredLength = GetRequiredLength(width, height, rowStride);
Guard.IsTrue(pixelMemory.Length >= requiredLength, nameof(pixelMemory), "The length of the input memory is less than the specified image size");
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(pixelMemory);
return new Image<TPixel>(configuration, memorySource, width, height, rowStride, metadata);
}
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<TPixel> pixelMemory,
@ -89,29 +116,17 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(configuration, pixelMemory, width, height, new ImageMetadata());
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input <see cref="Memory{T}"/>
/// to the new <see cref="Image{TPixel}"/> instance. This means that consumers of this method must ensure that the input buffer
/// is either self-contained, (for example, a <see cref="Memory{T}"/> instance wrapping a new array that was
/// created), or that the owning object is not disposed until the returned <see cref="Image{TPixel}"/> is disposed.
/// </para>
/// <para>
/// If the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="pixelMemory">The pixel memory.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<TPixel> pixelMemory,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(configuration, pixelMemory, width, height, rowStride, new ImageMetadata());
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<TPixel> pixelMemory,
int width,
@ -119,6 +134,15 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemory, width, height);
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<TPixel> pixelMemory,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemory, width, height, rowStride);
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
@ -152,19 +176,55 @@ public abstract partial class Image
}
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="pixelMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="pixelMemoryOwner"/>.
/// It will be disposed together with the result image.
/// <para>
/// Wraps an existing memory owner allowing viewing/manipulation as an <see cref="Image{TPixel}"/>
/// with <paramref name="rowStride"/> pixels between row starts.
/// </para>
/// <para>
/// Ownership of <paramref name="pixelMemoryOwner"/> is transferred to the returned image. The caller
/// must not dispose the owner manually.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="pixelMemoryOwner">The <see cref="IMemoryOwner{T}"/> that is being transferred to the image.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <param name="pixelMemoryOwner">The pixel memory owner transferred to the image.</param>
/// <param name="width">The width of the memory image in pixels.</param>
/// <param name="height">The height of the memory image in pixels.</param>
/// <param name="rowStride">The number of pixels between row starts in the source memory.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance</returns>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStride"/> is less than <paramref name="width"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// The length of <paramref name="pixelMemoryOwner"/> is less than
/// <c>((height - 1) * rowStride) + width</c>.
/// </exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<TPixel> pixelMemoryOwner,
int width,
int height,
int rowStride,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride));
long requiredLength = GetRequiredLength(width, height, rowStride);
Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size");
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(pixelMemoryOwner);
return new Image<TPixel>(configuration, memorySource, width, height, rowStride, metadata);
}
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{TPixel}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<TPixel> pixelMemoryOwner,
@ -173,18 +233,17 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(configuration, pixelMemoryOwner, width, height, new ImageMetadata());
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="pixelMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="pixelMemoryOwner"/>.
/// It will be disposed together with the result image.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="pixelMemoryOwner">The <see cref="IMemoryOwner{T}"/> that is being transferred to the image.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{TPixel}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<TPixel> pixelMemoryOwner,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(configuration, pixelMemoryOwner, width, height, rowStride, new ImageMetadata());
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{TPixel}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
IMemoryOwner<TPixel> pixelMemoryOwner,
int width,
@ -192,6 +251,15 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemoryOwner, width, height);
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{TPixel}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
IMemoryOwner<TPixel> pixelMemoryOwner,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemoryOwner, width, height, rowStride);
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
@ -240,29 +308,56 @@ public abstract partial class Image
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// Wraps an existing byte memory area allowing viewing/manipulation as an <see cref="Image{TPixel}"/>
/// with <paramref name="rowStrideInBytes"/> bytes between row starts.
/// </para>
/// <para>
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input <see cref="Memory{T}"/>
/// to the new <see cref="Image{TPixel}"/> instance. This means that consumers of this method must ensure that the input buffer
/// is either self-contained, (for example, a <see cref="Memory{T}"/> instance wrapping a new array that was
/// created), or that the owning object is not disposed until the returned <see cref="Image{TPixel}"/> is disposed.
/// </para>
/// <para>
/// If the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately.
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input
/// <see cref="Memory{T}"/> to the new <see cref="Image{TPixel}"/> instance. Consumers must ensure that
/// the input buffer remains valid for the full lifetime of the returned image.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <param name="byteMemory">The source byte memory.</param>
/// <param name="width">The width of the memory image in pixels.</param>
/// <param name="height">The height of the memory image in pixels.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in <paramref name="byteMemory"/>.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStrideInBytes"/> resolves to less than <paramref name="width"/> pixels.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="rowStrideInBytes"/> is not divisible by the size of <typeparamref name="TPixel"/>,
/// or <paramref name="byteMemory"/> is smaller than the required strided image length.
/// </exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
int width,
int height,
int rowStrideInBytes,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
int rowStride = GetPixelRowStrideFromByteStride<TPixel>(width, rowStrideInBytes, nameof(rowStrideInBytes));
long requiredLength = GetRequiredLength(width, height, rowStride);
ByteMemoryManager<TPixel> memoryManager = new(byteMemory);
Guard.IsTrue(memoryManager.Memory.Length >= requiredLength, nameof(byteMemory), "The length of the input memory is less than the specified image size");
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(memoryManager.Memory);
return new Image<TPixel>(configuration, memorySource, width, height, rowStride, metadata);
}
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
@ -271,29 +366,17 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemory, width, height, new ImageMetadata());
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please note: using this method does not transfer the ownership of the underlying buffer of the input <see cref="Memory{T}"/>
/// to the new <see cref="Image{TPixel}"/> instance. This means that consumers of this method must ensure that the input buffer
/// is either self-contained, (for example, a <see cref="Memory{T}"/> instance wrapping a new array that was
/// created), or that the owning object is not disposed until the returned <see cref="Image{TPixel}"/> is disposed.
/// </para>
/// <para>
/// If the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemory, width, height, rowStrideInBytes, new ImageMetadata());
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<byte> byteMemory,
int width,
@ -301,6 +384,15 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemory, width, height);
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<byte> byteMemory,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemory, width, height, rowStrideInBytes);
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
@ -337,19 +429,56 @@ public abstract partial class Image
}
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="byteMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="byteMemoryOwner"/>.
/// It will be disposed together with the result image.
/// <para>
/// Wraps an existing byte memory owner allowing viewing/manipulation as an <see cref="Image{TPixel}"/>
/// with <paramref name="rowStrideInBytes"/> bytes between row starts.
/// </para>
/// <para>
/// Ownership of <paramref name="byteMemoryOwner"/> is transferred to the returned image. The caller
/// must not dispose the owner manually.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemoryOwner">The <see cref="IMemoryOwner{T}"/> that is being transferred to the image.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <param name="byteMemoryOwner">The byte memory owner transferred to the image.</param>
/// <param name="width">The width of the memory image in pixels.</param>
/// <param name="height">The height of the memory image in pixels.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in the source memory.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance</returns>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStrideInBytes"/> resolves to less than <paramref name="width"/> pixels.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="rowStrideInBytes"/> is not divisible by the size of <typeparamref name="TPixel"/>,
/// or <paramref name="byteMemoryOwner"/> is smaller than the required strided image length.
/// </exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<byte> byteMemoryOwner,
int width,
int height,
int rowStrideInBytes,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
int rowStride = GetPixelRowStrideFromByteStride<TPixel>(width, rowStrideInBytes, nameof(rowStrideInBytes));
ByteMemoryOwner<TPixel> pixelMemoryOwner = new(byteMemoryOwner);
long requiredLength = GetRequiredLength(width, height, rowStride);
Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(byteMemoryOwner), "The length of the input memory is less than the specified image size");
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(pixelMemoryOwner);
return new Image<TPixel>(configuration, memorySource, width, height, rowStride, metadata);
}
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{byte}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<byte> byteMemoryOwner,
@ -358,18 +487,17 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemoryOwner, width, height, new ImageMetadata());
/// <summary>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="byteMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="byteMemoryOwner"/>.
/// It will be disposed together with the result image.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="byteMemoryOwner">The <see cref="IMemoryOwner{T}"/> that is being transferred to the image.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{byte}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
IMemoryOwner<byte> byteMemoryOwner,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemoryOwner, width, height, rowStrideInBytes, new ImageMetadata());
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{byte}, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
IMemoryOwner<byte> byteMemoryOwner,
int width,
@ -377,6 +505,15 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemoryOwner, width, height);
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, IMemoryOwner{byte}, int, int, int, ImageMetadata)"/>
public static Image<TPixel> WrapMemory<TPixel>(
IMemoryOwner<byte> byteMemoryOwner,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemoryOwner, width, height, rowStrideInBytes);
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
@ -434,35 +571,60 @@ public abstract partial class Image
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please note: this method relies on callers to carefully manage the target memory area being referenced by the
/// pointer and that the lifetime of such a memory area is at least equal to that of the returned
/// <see cref="Image{TPixel}"/> instance. For example, if the input pointer references an unmanaged memory area,
/// callers must ensure that the memory area is not freed as long as the returned <see cref="Image{TPixel}"/> is
/// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers
/// must ensure that objects will remain pinned as long as the <see cref="Image{TPixel}"/> instance is in use.
/// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes.
/// Wraps an unmanaged memory area allowing viewing/manipulation as an <see cref="Image{TPixel}"/>
/// with <paramref name="rowStrideInBytes"/> bytes between row starts.
/// </para>
/// <para>
/// Note also that if you have a <see cref="Memory{T}"/> or an array (which can be cast to <see cref="Memory{T}"/>) of
/// either <see cref="byte"/> or <typeparamref name="TPixel"/> values, it is highly recommended to use one of the other
/// available overloads of this method instead (such as <see cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int)"/>
/// or <see cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int)"/>, to make the resulting code less error
/// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when
/// doing interop or working with buffers that are located in unmanaged memory.
/// Callers must ensure the memory referenced by <paramref name="pointer"/> remains valid for the full
/// lifetime of the returned image.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="pointer">The pointer to the target memory buffer to wrap.</param>
/// <param name="bufferSizeInBytes">The byte length of the memory allocated.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <param name="pointer">The pointer to the source memory.</param>
/// <param name="bufferSizeInBytes">The byte length of the source memory.</param>
/// <param name="width">The width of the memory image in pixels.</param>
/// <param name="height">The height of the memory image in pixels.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in the source memory.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="pointer"/> is null,
/// <paramref name="rowStrideInBytes"/> is not divisible by the size of <typeparamref name="TPixel"/>,
/// or <paramref name="bufferSizeInBytes"/> is smaller than the required strided image length.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or <paramref name="rowStrideInBytes"/> resolves to less than <paramref name="width"/> pixels.
/// </exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static unsafe Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
void* pointer,
int bufferSizeInBytes,
int width,
int height,
int rowStrideInBytes,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must be not null");
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
int rowStride = GetPixelRowStrideFromByteStride<TPixel>(width, rowStrideInBytes, nameof(rowStrideInBytes));
long requiredLength = GetRequiredLength(width, height, rowStride);
Guard.MustBeLessThanOrEqualTo(requiredLength, int.MaxValue, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / Unsafe.SizeOf<TPixel>(), requiredLength, nameof(bufferSizeInBytes));
UnmanagedMemoryManager<TPixel> memoryManager = new(pointer, (int)requiredLength);
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(memoryManager.Memory);
return new Image<TPixel>(configuration, memorySource, width, height, rowStride, metadata);
}
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, void*, int, int, int, ImageMetadata)"/>
public static unsafe Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
void* pointer,
@ -472,35 +634,18 @@ public abstract partial class Image
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, pointer, bufferSizeInBytes, width, height, new ImageMetadata());
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please note: this method relies on callers to carefully manage the target memory area being referenced by the
/// pointer and that the lifetime of such a memory area is at least equal to that of the returned
/// <see cref="Image{TPixel}"/> instance. For example, if the input pointer references an unmanaged memory area,
/// callers must ensure that the memory area is not freed as long as the returned <see cref="Image{TPixel}"/> is
/// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers
/// must ensure that objects will remain pinned as long as the <see cref="Image{TPixel}"/> instance is in use.
/// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes.
/// </para>
/// <para>
/// Note also that if you have a <see cref="Memory{T}"/> or an array (which can be cast to <see cref="Memory{T}"/>) of
/// either <see cref="byte"/> or <typeparamref name="TPixel"/> values, it is highly recommended to use one of the other
/// available overloads of this method instead (such as <see cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int)"/>
/// or <see cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int)"/>, to make the resulting code less error
/// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when
/// doing interop or working with buffers that are located in unmanaged memory.
/// </para>
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="pointer">The pointer to the target memory buffer to wrap.</param>
/// <param name="bufferSizeInBytes">The byte length of the memory allocated.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, void*, int, int, int, int, ImageMetadata)"/>
public static unsafe Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
void* pointer,
int bufferSizeInBytes,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, pointer, bufferSizeInBytes, width, height, rowStrideInBytes, new ImageMetadata());
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, void*, int, int, int, ImageMetadata)"/>
public static unsafe Image<TPixel> WrapMemory<TPixel>(
void* pointer,
int bufferSizeInBytes,
@ -508,4 +653,41 @@ public abstract partial class Image
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, pointer, bufferSizeInBytes, width, height);
/// <inheritdoc cref="WrapMemory{TPixel}(Configuration, void*, int, int, int, int, ImageMetadata)"/>
public static unsafe Image<TPixel> WrapMemory<TPixel>(
void* pointer,
int bufferSizeInBytes,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, pointer, bufferSizeInBytes, width, height, rowStrideInBytes);
private static void ValidateWrapMemoryStride(int width, int height, int rowStride, string rowStrideParamName)
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName);
}
private static int GetPixelRowStrideFromByteStride<TPixel>(int width, int rowStrideInBytes, string rowStrideParamName)
where TPixel : unmanaged, IPixel<TPixel>
{
int pixelSizeInBytes = Unsafe.SizeOf<TPixel>();
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(rowStrideInBytes, 0, rowStrideParamName);
Guard.IsTrue(
rowStrideInBytes % pixelSizeInBytes == 0,
rowStrideParamName,
"The row stride in bytes must be divisible by the pixel size.");
int rowStride = rowStrideInBytes / pixelSizeInBytes;
Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName);
return rowStride;
}
private static long GetRequiredLength(int width, int height, int rowStride)
=> checked(((long)(height - 1) * rowStride) + width);
}

63
src/ImageSharp/ImageFrame.LoadPixelData.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -25,6 +26,36 @@ public partial class ImageFrame
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(configuration, MemoryMarshal.Cast<byte, TPixel>(data), width, height);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the given byte array in <typeparamref name="TPixel"/> format.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="data">The byte array containing image data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStrideInBytes">The number of bytes between row starts in <paramref name="data"/>.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
internal static ImageFrame<TPixel> LoadPixelData<TPixel>(
Configuration configuration,
ReadOnlySpan<byte> data,
int width,
int height,
int rowStrideInBytes)
where TPixel : unmanaged, IPixel<TPixel>
{
int pixelSizeInBytes = Unsafe.SizeOf<TPixel>();
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(rowStrideInBytes, 0, nameof(rowStrideInBytes));
Guard.IsTrue(
rowStrideInBytes % pixelSizeInBytes == 0,
nameof(rowStrideInBytes),
"The row stride in bytes must be divisible by the pixel size.");
int rowStride = rowStrideInBytes / pixelSizeInBytes;
return LoadPixelData(configuration, MemoryMarshal.Cast<byte, TPixel>(data), width, height, rowStride);
}
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from the raw <typeparamref name="TPixel"/> data.
/// </summary>
@ -36,14 +67,36 @@ public partial class ImageFrame
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
internal static ImageFrame<TPixel> LoadPixelData<TPixel>(Configuration configuration, ReadOnlySpan<TPixel> data, int width, int height)
where TPixel : unmanaged, IPixel<TPixel>
=> LoadPixelData(configuration, data, width, height, width);
/// <summary>
/// Create a new instance of the <see cref="Image{TPixel}"/> class from raw <typeparamref name="TPixel"/> data
/// using <paramref name="rowStride"/> pixels between source row starts.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="data">The span containing the image pixel data.</param>
/// <param name="width">The width of the final image.</param>
/// <param name="height">The height of the final image.</param>
/// <param name="rowStride">The number of pixels between row starts in <paramref name="data"/>.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>A new <see cref="Image{TPixel}"/>.</returns>
internal static ImageFrame<TPixel> LoadPixelData<TPixel>(
Configuration configuration,
ReadOnlySpan<TPixel> data,
int width,
int height,
int rowStride)
where TPixel : unmanaged, IPixel<TPixel>
{
int count = width * height;
Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data));
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride));
ImageFrame<TPixel> image = new(configuration, width, height);
long requiredLength = checked(((long)(height - 1) * rowStride) + width);
Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data));
data = data[..count];
data.CopyTo(image.PixelBuffer.FastMemoryGroup);
ImageFrame<TPixel> image = new(configuration, width, height);
image.PixelBuffer.CopyFrom(data, rowStride);
return image;
}

2
src/ImageSharp/ImageFrame.cs

@ -71,7 +71,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <param name="disposing">Whether to dispose of managed and unmanaged objects.</param>
protected abstract void Dispose(bool disposing);
internal abstract void CopyPixelsTo<TDestinationPixel>(MemoryGroup<TDestinationPixel> destination)
internal abstract void CopyPixelsTo<TDestinationPixel>(Buffer2D<TDestinationPixel> destination)
where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>;
/// <summary>

9
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -27,11 +27,16 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
}
internal ImageFrameCollection(Image<TPixel> parent, int width, int height, MemoryGroup<TPixel> memorySource)
: this(parent, width, height, width, memorySource)
{
}
internal ImageFrameCollection(Image<TPixel> parent, int width, int height, int rowStride, MemoryGroup<TPixel> memorySource)
{
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
// Frames are already cloned within the caller
this.frames.Add(new ImageFrame<TPixel>(parent.Configuration, width, height, memorySource));
this.frames.Add(new ImageFrame<TPixel>(parent.Configuration, width, height, rowStride, memorySource));
}
internal ImageFrameCollection(Image<TPixel> parent, IEnumerable<ImageFrame<TPixel>> frames)
@ -416,7 +421,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.parent.Configuration,
source.Size,
source.Metadata.DeepClone());
source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup);
source.CopyPixelsTo(result.PixelBuffer);
return result;
}
}

99
src/ImageSharp/ImageFrame{TPixel}.cs

@ -114,7 +114,20 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="memorySource">The memory source.</param>
internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup<TPixel> memorySource)
: this(configuration, width, height, memorySource, new ImageFrameMetadata())
: this(configuration, width, height, width, memorySource, new ImageFrameMetadata())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class wrapping an existing buffer.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="rowStride">The number of elements between row starts.</param>
/// <param name="memorySource">The memory source.</param>
internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup<TPixel> memorySource)
: this(configuration, width, height, rowStride, memorySource, new ImageFrameMetadata())
{
}
@ -127,12 +140,26 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <param name="memorySource">The memory source.</param>
/// <param name="metadata">The metadata.</param>
internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup<TPixel> memorySource, ImageFrameMetadata metadata)
: this(configuration, width, height, width, memorySource, metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class wrapping an existing buffer.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="rowStride">The number of elements between row starts.</param>
/// <param name="memorySource">The memory source.</param>
/// <param name="metadata">The metadata.</param>
internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup<TPixel> memorySource, ImageFrameMetadata metadata)
: base(configuration, width, height, metadata)
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.PixelBuffer = new Buffer2D<TPixel>(memorySource, width, height);
this.PixelBuffer = new Buffer2D<TPixel>(memorySource, width, height, rowStride);
}
/// <summary>
@ -150,7 +177,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
source.PixelBuffer.Width,
source.PixelBuffer.Height,
configuration.PreferContiguousImageBuffers);
source.PixelBuffer.FastMemoryGroup.CopyTo(this.PixelBuffer.FastMemoryGroup);
source.PixelBuffer.CopyTo(this.PixelBuffer);
}
/// <inheritdoc/>
@ -270,16 +297,23 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
}
/// <summary>
/// Copy image pixels to <paramref name="destination"/>.
/// Copy image pixels to <paramref name="destination"/> using the backing row layout.
/// </summary>
/// <remarks>
/// Destination length must be at least <c>((Height - 1) * PixelBuffer.RowStride) + Width</c>.
/// </remarks>
/// <param name="destination">The <see cref="Span{TPixel}"/> to copy image pixels to.</param>
public void CopyPixelDataTo(Span<TPixel> destination) => this.GetPixelMemoryGroup().CopyTo(destination);
public void CopyPixelDataTo(Span<TPixel> destination) => this.PixelBuffer.CopyTo(destination);
/// <summary>
/// Copy image pixels to <paramref name="destination"/>.
/// Copy image pixels to <paramref name="destination"/> using the backing row layout.
/// </summary>
/// <remarks>
/// Destination length must be at least
/// <c>(((Height - 1) * PixelBuffer.RowStride) + Width) * sizeof(TPixel)</c> bytes.
/// </remarks>
/// <param name="destination">The <see cref="Span{T}"/> of <see cref="byte"/> to copy image pixels to.</param>
public void CopyPixelDataTo(Span<byte> destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast<byte, TPixel>(destination));
public void CopyPixelDataTo(Span<byte> destination) => this.PixelBuffer.CopyTo(MemoryMarshal.Cast<byte, TPixel>(destination));
/// <summary>
/// Gets the representation of the pixels as a <see cref="Memory{T}"/> in the source image's pixel format
@ -294,17 +328,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <param name="memory">The <see cref="Memory{T}"/> referencing the image buffer.</param>
/// <returns>The <see cref="bool"/> indicating the success.</returns>
public bool DangerousTryGetSinglePixelMemory(out Memory<TPixel> memory)
{
IMemoryGroup<TPixel> mg = this.GetPixelMemoryGroup();
if (mg.Count > 1)
{
memory = default;
return false;
}
memory = mg.Single();
return true;
}
=> this.PixelBuffer.DangerousTryGetSingleMemory(out memory);
/// <summary>
/// Gets a reference to the pixel at the specified position.
@ -327,7 +351,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target));
}
this.PixelBuffer.FastMemoryGroup.CopyTo(target.FastMemoryGroup);
this.PixelBuffer.CopyTo(target);
}
/// <summary>
@ -370,20 +394,32 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
this.isDisposed = true;
}
internal override void CopyPixelsTo<TDestinationPixel>(MemoryGroup<TDestinationPixel> destination)
internal override void CopyPixelsTo<TDestinationPixel>(Buffer2D<TDestinationPixel> destination)
{
Guard.NotNull(destination, nameof(destination));
Guard.IsTrue(
destination.Width == this.Width && destination.Height == this.Height,
nameof(destination),
"Destination buffer must have the same dimensions as the source frame.");
if (typeof(TPixel) == typeof(TDestinationPixel))
{
this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) =>
for (int y = 0; y < this.Height; y++)
{
Span<TPixel> d1 = MemoryMarshal.Cast<TDestinationPixel, TPixel>(d);
s.CopyTo(d1);
});
Span<TPixel> sourceRow = this.PixelBuffer.DangerousGetRowSpan(y);
Span<TDestinationPixel> destinationRow = destination.DangerousGetRowSpan(y);
sourceRow.CopyTo(MemoryMarshal.Cast<TDestinationPixel, TPixel>(destinationRow));
}
return;
}
this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d)
=> PixelOperations<TPixel>.Instance.To(this.Configuration, s, d));
for (int y = 0; y < this.Height; y++)
{
Span<TPixel> sourceRow = this.PixelBuffer.DangerousGetRowSpan(y);
Span<TDestinationPixel> destinationRow = destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.To(this.Configuration, sourceRow, destinationRow);
}
}
/// <inheritdoc/>
@ -441,16 +477,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(TPixel value)
{
MemoryGroup<TPixel> group = this.PixelBuffer.FastMemoryGroup;
if (value.Equals(default))
{
group.Clear();
}
else
{
group.Fill(value);
}
this.PixelBuffer.Clear(value);
}
[MethodImpl(InliningOptions.ShortMethod)]

53
src/ImageSharp/Image{TPixel}.cs

@ -91,7 +91,7 @@ public sealed class Image<TPixel> : Image
Configuration configuration,
Buffer2D<TPixel> pixelBuffer,
ImageMetadata metadata)
: this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata)
: this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, pixelBuffer.RowStride, metadata)
{
}
@ -110,8 +110,29 @@ public sealed class Image<TPixel> : Image
int width,
int height,
ImageMetadata metadata)
: this(configuration, memoryGroup, width, height, width, metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// wrapping an external <see cref="MemoryGroup{T}"/>.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="memoryGroup">The memory source.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="rowStride">The number of elements between row starts.</param>
/// <param name="metadata">The images metadata.</param>
internal Image(
Configuration configuration,
MemoryGroup<TPixel> memoryGroup,
int width,
int height,
int rowStride,
ImageMetadata metadata)
: base(configuration, TPixel.GetPixelTypeInfo(), metadata, width, height)
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, memoryGroup);
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, rowStride, memoryGroup);
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -287,16 +308,24 @@ public sealed class Image<TPixel> : Image
}
/// <summary>
/// Copy image pixels to <paramref name="destination"/>.
/// Copy image pixels to <paramref name="destination"/> using the root frame backing row layout.
/// </summary>
/// <remarks>
/// Destination length must be at least
/// <c>((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width</c>.
/// </remarks>
/// <param name="destination">The <see cref="Span{TPixel}"/> to copy image pixels to.</param>
public void CopyPixelDataTo(Span<TPixel> destination) => this.GetPixelMemoryGroup().CopyTo(destination);
public void CopyPixelDataTo(Span<TPixel> destination) => this.Frames.RootFrame.CopyPixelDataTo(destination);
/// <summary>
/// Copy image pixels to <paramref name="destination"/>.
/// Copy image pixels to <paramref name="destination"/> using the root frame backing row layout.
/// </summary>
/// <remarks>
/// Destination length must be at least
/// <c>(((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width) * sizeof(TPixel)</c> bytes.
/// </remarks>
/// <param name="destination">The <see cref="Span{T}"/> of <see cref="byte"/> to copy image pixels to.</param>
public void CopyPixelDataTo(Span<byte> destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast<byte, TPixel>(destination));
public void CopyPixelDataTo(Span<byte> destination) => this.Frames.RootFrame.CopyPixelDataTo(destination);
/// <summary>
/// Gets the representation of the pixels as a <see cref="Memory{T}"/> in the source image's pixel format
@ -311,17 +340,7 @@ public sealed class Image<TPixel> : Image
/// <param name="memory">The <see cref="Memory{T}"/> referencing the image buffer.</param>
/// <returns>The <see cref="bool"/> indicating the success.</returns>
public bool DangerousTryGetSinglePixelMemory(out Memory<TPixel> memory)
{
IMemoryGroup<TPixel> mg = this.GetPixelMemoryGroup();
if (mg.Count > 1)
{
memory = default;
return false;
}
memory = mg.Single();
return true;
}
=> this.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out memory);
/// <summary>
/// Clones the current image.

6
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -45,7 +45,7 @@ public static class Buffer2DExtensions
Buffer2DRegion<T> sourceRegion = source.GetRegion(rectangle);
if (sourceRegion.IsFullBufferArea)
{
sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup);
sourceRegion.Buffer.CopyTo(buffer);
}
else
{
@ -81,7 +81,7 @@ public static class Buffer2DExtensions
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount);
int elementSize = Unsafe.SizeOf<T>();
int width = buffer.Width * elementSize;
int rowByteStride = buffer.RowStride * elementSize;
int sOffset = sourceIndex * elementSize;
int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize;
@ -98,7 +98,7 @@ public static class Buffer2DExtensions
Buffer.MemoryCopy(sPtr, dPtr, count, count);
basePtr += width;
basePtr += rowByteStride;
}
}
}

12
src/ImageSharp/Memory/Buffer2DRegion{T}.cs

@ -59,9 +59,9 @@ public readonly struct Buffer2DRegion<T>
public int Height => this.Rectangle.Height;
/// <summary>
/// Gets the pixel stride which is equal to the width of <see cref="Buffer"/>.
/// Gets the number of elements between row starts in <see cref="Buffer"/>.
/// </summary>
public int Stride => this.Buffer.Width;
public int Stride => this.Buffer.RowStride;
/// <summary>
/// Gets the size of the area.
@ -146,9 +146,9 @@ public readonly struct Buffer2DRegion<T>
internal void Clear()
{
// Optimization for when the size of the area is the same as the buffer size.
if (this.IsFullBufferArea)
if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width)
{
this.Buffer.FastMemoryGroup.Clear();
this.Buffer.Clear(default);
return;
}
@ -166,9 +166,9 @@ public readonly struct Buffer2DRegion<T>
internal void Fill(T value)
{
// Optimization for when the size of the area is the same as the buffer size.
if (this.IsFullBufferArea)
if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width)
{
this.Buffer.FastMemoryGroup.Fill(value);
this.Buffer.Clear(value);
return;
}

229
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Memory;
@ -20,10 +21,27 @@ public sealed class Buffer2D<T> : IDisposable
/// <param name="width">The number of elements in a row.</param>
/// <param name="height">The number of rows.</param>
internal Buffer2D(MemoryGroup<T> memoryGroup, int width, int height)
: this(memoryGroup, width, height, width)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Buffer2D{T}"/> class.
/// </summary>
/// <param name="memoryGroup">The <see cref="MemoryGroup{T}"/> to wrap.</param>
/// <param name="width">The number of elements in a row.</param>
/// <param name="height">The number of rows.</param>
/// <param name="rowStride">The number of elements between row starts.</param>
internal Buffer2D(MemoryGroup<T> memoryGroup, int width, int height, int rowStride)
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride));
this.FastMemoryGroup = memoryGroup;
this.Width = width;
this.Height = height;
this.RowStride = rowStride;
}
/// <summary>
@ -36,6 +54,11 @@ public sealed class Buffer2D<T> : IDisposable
/// </summary>
public int Height { get; private set; }
/// <summary>
/// Gets the number of elements between row starts in the backing memory.
/// </summary>
public int RowStride { get; private set; }
/// <summary>
/// Gets the backing <see cref="IMemoryGroup{T}"/>.
/// </summary>
@ -75,6 +98,168 @@ public sealed class Buffer2D<T> : IDisposable
}
}
/// <summary>
/// Wraps an existing memory area as a <see cref="Buffer2D{T}"/> with tightly packed rows.
/// </summary>
/// <remarks>
/// This method does not transfer ownership of <paramref name="memory"/> to the returned <see cref="Buffer2D{T}"/>.
/// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer.
/// If <paramref name="memory"/> originates from an <see cref="IMemoryOwner{T}"/> (for example from <see cref="MemoryPool{T}"/>),
/// do not dispose that owner while the returned buffer is still in use.
/// </remarks>
/// <param name="memory">The source memory.</param>
/// <param name="width">The number of elements in each row.</param>
/// <param name="height">The number of rows.</param>
/// <returns>The wrapped <see cref="Buffer2D{T}"/> instance.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="width"/> or <paramref name="height"/> is not positive.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="memory"/> is shorter than <c>width * height</c>.</exception>
#pragma warning disable CA1000 // Do not declare static members on generic types
public static Buffer2D<T> WrapMemory(Memory<T> memory, int width, int height)
#pragma warning restore CA1000 // Do not declare static members on generic types
=> WrapMemory(memory, width, height, width);
/// <summary>
/// Wraps an existing memory area as a <see cref="Buffer2D{T}"/> using the specified row stride.
/// </summary>
/// <remarks>
/// This method does not transfer ownership of <paramref name="memory"/> to the returned <see cref="Buffer2D{T}"/>.
/// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer.
/// If <paramref name="memory"/> originates from an <see cref="IMemoryOwner{T}"/> (for example from <see cref="MemoryPool{T}"/>),
/// do not dispose that owner while the returned buffer is still in use.
/// The minimum required length is <c>((height - 1) * stride) + width</c> elements.
/// </remarks>
/// <param name="memory">The source memory.</param>
/// <param name="width">The number of elements in each row.</param>
/// <param name="height">The number of rows.</param>
/// <param name="stride">The number of elements between row starts in the source memory.</param>
/// <returns>The wrapped <see cref="Buffer2D{T}"/> instance.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="width"/> or <paramref name="height"/> is not positive,
/// or when <paramref name="stride"/> is less than <paramref name="width"/>.
/// </exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="memory"/> is shorter than the required buffer size.</exception>
#pragma warning disable CA1000 // Do not declare static members on generic types
public static Buffer2D<T> WrapMemory(Memory<T> memory, int width, int height, int stride)
#pragma warning restore CA1000 // Do not declare static members on generic types
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(stride, width, nameof(stride));
long requiredLength = checked(((long)(height - 1) * stride) + width);
Guard.IsTrue(memory.Length >= requiredLength, nameof(memory), "The length of the input memory is less than the specified buffer size");
MemoryGroup<T> memorySource = MemoryGroup<T>.Wrap(memory);
return new Buffer2D<T>(memorySource, width, height, stride);
}
/// <summary>
/// Gets the representation of the values as a single contiguous <see cref="Memory{T}"/>
/// when the backing group is a single tightly packed segment.
/// </summary>
/// <param name="memory">The <see cref="Memory{T}"/> referencing the buffer.</param>
/// <returns>
/// <see langword="true"/> when the buffer can be copied as one contiguous block
/// without per-row handling; otherwise <see langword="false"/>.
/// </returns>
public bool DangerousTryGetSingleMemory(out Memory<T> memory)
{
if (this.MemoryGroup.Count > 1 || this.RowStride != this.Width)
{
memory = default;
return false;
}
int logicalLength = checked((int)((long)this.Width * this.Height));
memory = this.MemoryGroup[0][..logicalLength];
return true;
}
/// <summary>
/// Copies this buffer into <paramref name="destination"/> using the source logical row layout.
/// </summary>
/// <remarks>
/// When dimensions are equal, destination stride is respected.
/// When dimensions differ, source stride is used to copy the source logical layout into destination memory.
/// </remarks>
/// <param name="destination">The destination buffer.</param>
internal void CopyTo(Buffer2D<T> destination)
{
Guard.NotNull(destination, nameof(destination));
bool sameDimensions = this.Width == destination.Width && this.Height == destination.Height;
int destinationStride = sameDimensions ? destination.RowStride : this.RowStride;
// Different dimensions use source logical layout. This supports SwapOrCopyContent,
// where metadata is swapped after data copy.
this.FastMemoryGroup.CopyTo(
this.RowStride,
destination.FastMemoryGroup,
destinationStride,
this.Width,
this.Height);
}
/// <summary>
/// Copies this buffer into <paramref name="destination"/> using the source row stride as destination layout.
/// </summary>
/// <param name="destination">The destination span.</param>
internal void CopyTo(Span<T> destination)
{
long requiredLength = checked(((long)(this.Height - 1) * this.RowStride) + this.Width);
Guard.MustBeGreaterThanOrEqualTo(destination.Length, requiredLength, nameof(destination));
this.FastMemoryGroup.CopyTo(
this.RowStride,
destination,
this.RowStride,
this.Width,
this.Height);
}
/// <summary>
/// Copies tightly packed row-major data from <paramref name="source"/> into this buffer.
/// </summary>
/// <param name="source">The source data.</param>
internal void CopyFrom(ReadOnlySpan<T> source) => this.CopyFrom(source, this.Width);
/// <summary>
/// Copies row-major data from <paramref name="source"/> into this buffer using
/// <paramref name="sourceStride"/> elements between source row starts.
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="sourceStride">The number of elements between source row starts.</param>
internal void CopyFrom(ReadOnlySpan<T> source, int sourceStride)
{
Guard.MustBeGreaterThanOrEqualTo(sourceStride, this.Width, nameof(sourceStride));
long requiredLength = checked(((long)(this.Height - 1) * sourceStride) + this.Width);
Guard.MustBeGreaterThanOrEqualTo(source.Length, requiredLength, nameof(source));
// Copy row by row so padded source rows map correctly into the destination logical rows.
int sourceOffset = 0;
for (int y = 0; y < this.Height; y++)
{
source.Slice(sourceOffset, this.Width).CopyTo(this.DangerousGetRowSpan(y));
sourceOffset += sourceStride;
}
}
/// <summary>
/// Clears this buffer when <paramref name="value"/> is default; otherwise fills it with <paramref name="value"/>.
/// </summary>
/// <param name="value">The fill value.</param>
internal void Clear(T value)
{
if (value.Equals(default))
{
this.FastMemoryGroup.Clear();
return;
}
this.FastMemoryGroup.Fill(value);
}
/// <summary>
/// Disposes the <see cref="Buffer2D{T}"/> instance
/// </summary>
@ -102,7 +287,13 @@ public sealed class Buffer2D<T> : IDisposable
this.ThrowYOutOfRangeException(y);
}
return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
if (this.RowStride == this.Width)
{
return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
}
int rowStart = checked(y * this.RowStride);
return this.FastMemoryGroup[0].Span.Slice(rowStart, this.Width);
}
internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span<T> paddedSpan)
@ -111,8 +302,10 @@ public sealed class Buffer2D<T> : IDisposable
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
int stride = this.Width + padding;
Span<T> slice = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
long rowStart = y * (long)this.RowStride;
Span<T> slice = this.RowStride == this.Width
? this.FastMemoryGroup.GetRemainingSliceOfBuffer(rowStart)
: this.FastMemoryGroup[0].Span[checked((int)rowStart)..];
if (slice.Length < stride)
{
@ -127,7 +320,10 @@ public sealed class Buffer2D<T> : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
internal ref T GetElementUnsafe(int x, int y)
{
Span<T> span = this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
Span<T> span = this.RowStride == this.Width
? this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width)
: this.FastMemoryGroup[0].Span.Slice(checked(y * this.RowStride), this.Width);
return ref span[x];
}
@ -141,6 +337,13 @@ public sealed class Buffer2D<T> : IDisposable
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
if (this.RowStride != this.Width)
{
int rowStart = checked(y * this.RowStride);
return this.FastMemoryGroup[0].Slice(rowStart, this.Width);
}
return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width);
}
@ -153,7 +356,7 @@ public sealed class Buffer2D<T> : IDisposable
/// Thrown when the backing group is discontiguous.
/// </exception>
[MethodImpl(InliningOptions.ShortMethod)]
internal Span<T> DangerousGetSingleSpan() => this.FastMemoryGroup.Single().Span;
internal Span<T> DangerousGetSingleSpan() => this.FastMemoryGroup[0].Span;
/// <summary>
/// Gets a <see cref="Memory{T}"/> to the backing data of if the backing group consists of a single contiguous memory buffer.
@ -164,7 +367,7 @@ public sealed class Buffer2D<T> : IDisposable
/// Thrown when the backing group is discontiguous.
/// </exception>
[MethodImpl(InliningOptions.ShortMethod)]
internal Memory<T> DangerousGetSingleMemory() => this.FastMemoryGroup.Single();
internal Memory<T> DangerousGetSingleMemory() => this.FastMemoryGroup[0];
/// <summary>
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
@ -185,21 +388,31 @@ public sealed class Buffer2D<T> : IDisposable
}
else
{
if (destination.FastMemoryGroup.TotalLength != source.FastMemoryGroup.TotalLength)
long sourceLayoutLength = GetRequiredLength(source.Width, source.Height, source.RowStride);
long destinationLayoutLength = GetRequiredLength(destination.Width, destination.Height, destination.RowStride);
bool destinationCanRepresentSource = destination.FastMemoryGroup.TotalLength >= sourceLayoutLength;
bool sourceCanRepresentDestination = source.FastMemoryGroup.TotalLength >= destinationLayoutLength;
if (!destinationCanRepresentSource || !sourceCanRepresentDestination)
{
throw new InvalidMemoryOperationException(
"Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images.");
}
source.FastMemoryGroup.CopyTo(destination.MemoryGroup);
source.CopyTo(destination);
}
(destination.Width, source.Width) = (source.Width, destination.Width);
(destination.Height, source.Height) = (source.Height, destination.Height);
(destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride);
return swapped;
}
[MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y)
=> throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
[MethodImpl(InliningOptions.ShortMethod)]
private static long GetRequiredLength(int width, int height, int stride)
=> ((long)(height - 1) * stride) + width;
}

191
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs

@ -71,128 +71,159 @@ internal static class MemoryGroupExtensions
return memory.Slice(bufferStart, length);
}
internal static void CopyTo<T>(this IMemoryGroup<T> source, Span<T> target)
/// <summary>
/// Copies a 2D logical region from <paramref name="source"/> into <paramref name="target"/>
/// using the provided source and target strides.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="source">The source memory group.</param>
/// <param name="sourceStride">Elements between source row starts.</param>
/// <param name="target">The destination span.</param>
/// <param name="targetStride">Elements between destination row starts.</param>
/// <param name="width">The logical row width to copy.</param>
/// <param name="height">The number of rows to copy.</param>
internal static void CopyTo<T>(
this IMemoryGroup<T> source,
int sourceStride,
Span<T> target,
int targetStride,
int width,
int height)
where T : struct
{
Guard.NotNull(source, nameof(source));
Guard.MustBeGreaterThanOrEqualTo(target.Length, source.TotalLength, nameof(target));
Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width));
Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride));
Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride));
MemoryGroupCursor<T> cur = new(source);
long position = 0;
while (position < source.TotalLength)
{
int fwd = Math.Min(cur.LookAhead(), target.Length);
cur.GetSpan(fwd).CopyTo(target);
long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width);
long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width);
Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source));
Guard.MustBeGreaterThanOrEqualTo(target.Length, targetRequired, nameof(target));
cur.Forward(fwd);
target = target[fwd..];
position += fwd;
if (width == 0 || height == 0)
{
return;
}
}
internal static void CopyTo<T>(this Span<T> source, IMemoryGroup<T> target)
where T : struct
=> CopyTo((ReadOnlySpan<T>)source, target);
MemoryGroupCursor<T> sourceCursor = new(source);
int sourceSkip = sourceStride - width;
internal static void CopyTo<T>(this ReadOnlySpan<T> source, IMemoryGroup<T> target)
where T : struct
{
Guard.NotNull(target, nameof(target));
Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, source.Length, nameof(target));
MemoryGroupCursor<T> cur = new(target);
while (!source.IsEmpty)
for (int y = 0; y < height; y++)
{
int fwd = Math.Min(cur.LookAhead(), source.Length);
source[..fwd].CopyTo(cur.GetSpan(fwd));
cur.Forward(fwd);
source = source[fwd..];
int rowStart = checked(y * targetStride);
Span<T> destinationRow = target.Slice(rowStart, width);
CopyFromCursorToSpan(ref sourceCursor, destinationRow);
// Trailing padding after the last row is optional, so only skip between rows.
if (y < height - 1)
{
ForwardCursor(ref sourceCursor, sourceSkip);
}
}
}
internal static void CopyTo<T>(this IMemoryGroup<T>? source, IMemoryGroup<T>? target)
/// <summary>
/// Copies a 2D logical region from <paramref name="source"/> into <paramref name="target"/>
/// using the provided source and target strides.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="source">The source memory group.</param>
/// <param name="sourceStride">Elements between source row starts.</param>
/// <param name="target">The destination memory group.</param>
/// <param name="targetStride">Elements between destination row starts.</param>
/// <param name="width">The logical row width to copy.</param>
/// <param name="height">The number of rows to copy.</param>
internal static void CopyTo<T>(
this IMemoryGroup<T> source,
int sourceStride,
IMemoryGroup<T> target,
int targetStride,
int width,
int height)
where T : struct
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(target, nameof(target));
Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid.");
Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid.");
Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!");
Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width));
Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height));
Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride));
Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride));
if (source.IsEmpty())
long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width);
long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width);
Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source));
Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, targetRequired, nameof(target));
if (width == 0 || height == 0)
{
return;
}
long position = 0;
MemoryGroupCursor<T> srcCur = new(source);
MemoryGroupCursor<T> trgCur = new(target);
MemoryGroupCursor<T> sourceCursor = new(source);
MemoryGroupCursor<T> targetCursor = new(target);
int sourceSkip = sourceStride - width;
int targetSkip = targetStride - width;
while (position < source.TotalLength)
for (int y = 0; y < height; y++)
{
int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead());
Span<T> srcSpan = srcCur.GetSpan(fwd);
Span<T> trgSpan = trgCur.GetSpan(fwd);
srcSpan.CopyTo(trgSpan);
srcCur.Forward(fwd);
trgCur.Forward(fwd);
position += fwd;
CopyFromCursorToCursor(ref sourceCursor, ref targetCursor, width);
// Trailing padding after the last row is optional, so only skip between rows.
if (y < height - 1)
{
ForwardCursor(ref sourceCursor, sourceSkip);
ForwardCursor(ref targetCursor, targetSkip);
}
}
}
internal static void TransformTo<TSource, TTarget>(
this IMemoryGroup<TSource> source,
IMemoryGroup<TTarget> target,
TransformItemsDelegate<TSource, TTarget> transform)
where TSource : struct
where TTarget : struct
private static void CopyFromCursorToCursor<T>(
ref MemoryGroupCursor<T> source,
ref MemoryGroupCursor<T> target,
int count)
where T : struct
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(target, nameof(target));
Guard.NotNull(transform, nameof(transform));
Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid.");
Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid.");
Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!");
if (source.IsEmpty())
int remaining = count;
while (remaining > 0)
{
return;
int fwd = Math.Min(remaining, Math.Min(source.LookAhead(), target.LookAhead()));
source.GetSpan(fwd).CopyTo(target.GetSpan(fwd));
source.Forward(fwd);
target.Forward(fwd);
remaining -= fwd;
}
}
long position = 0;
MemoryGroupCursor<TSource> srcCur = new(source);
MemoryGroupCursor<TTarget> trgCur = new(target);
while (position < source.TotalLength)
private static void CopyFromCursorToSpan<T>(ref MemoryGroupCursor<T> source, Span<T> target)
where T : struct
{
int remaining = target.Length;
while (remaining > 0)
{
int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead());
Span<TSource> srcSpan = srcCur.GetSpan(fwd);
Span<TTarget> trgSpan = trgCur.GetSpan(fwd);
transform(srcSpan, trgSpan);
srcCur.Forward(fwd);
trgCur.Forward(fwd);
position += fwd;
int copied = target.Length - remaining;
int fwd = Math.Min(remaining, source.LookAhead());
source.GetSpan(fwd).CopyTo(target[copied..]);
source.Forward(fwd);
remaining -= fwd;
}
}
internal static void TransformInplace<T>(
this IMemoryGroup<T> memoryGroup,
TransformItemsInplaceDelegate<T> transform)
private static void ForwardCursor<T>(ref MemoryGroupCursor<T> cursor, int steps)
where T : struct
{
foreach (Memory<T> memory in memoryGroup)
int remaining = steps;
while (remaining > 0)
{
transform(memory.Span);
int fwd = Math.Min(remaining, cursor.LookAhead());
cursor.Forward(fwd);
remaining -= fwd;
}
}
internal static bool IsEmpty<T>(this IMemoryGroup<T> group)
where T : struct
=> group.Count == 0;
private struct MemoryGroupCursor<T>
where T : struct
{

6
src/ImageSharp/Memory/MemoryAllocatorExtensions.cs

@ -29,6 +29,9 @@ public static class MemoryAllocatorExtensions
AllocationOptions options = AllocationOptions.None)
where T : struct
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
long groupLength = (long)width * height;
MemoryGroup<T> memoryGroup;
if (preferContiguosImageBuffers && groupLength < int.MaxValue)
@ -104,6 +107,9 @@ public static class MemoryAllocatorExtensions
AllocationOptions options = AllocationOptions.None)
where T : struct
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
long groupLength = (long)width * height;
MemoryGroup<T> memoryGroup = memoryAllocator.AllocateGroup<T>(
groupLength,

8
src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs

@ -1,8 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Memory;
#pragma warning disable SA1649 // File name should match first type name
internal delegate void TransformItemsDelegate<TSource, TTarget>(ReadOnlySpan<TSource> source, Span<TTarget> target);
#pragma warning restore SA1649 // File name should match first type name

6
src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs

@ -1,6 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Memory;
internal delegate void TransformItemsInplaceDelegate<T>(Span<T> data);

2
src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs

@ -53,7 +53,7 @@ internal class CropProcessor<TPixel> : TransformProcessor<TPixel>
&& this.SourceRectangle == this.cropRectangle)
{
// the cloned will be blank here copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
source.PixelBuffer.CopyTo(destination.PixelBuffer);
return;
}

2
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs

@ -97,7 +97,7 @@ internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
if (MathF.Abs(degrees) < Constants.Epsilon)
{
// The destination will be blank here so copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
source.PixelBuffer.CopyTo(destination.PixelBuffer);
return true;
}

2
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs

@ -91,7 +91,7 @@ internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResampling
ImageFrame<TPixel> destinationFrame = destination.Frames[i];
// The cloned will be blank here copy all the pixel data over
sourceFrame.GetPixelMemoryGroup().CopyTo(destinationFrame.GetPixelMemoryGroup());
sourceFrame.PixelBuffer.CopyTo(destinationFrame.PixelBuffer);
}
return;

2
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs

@ -64,7 +64,7 @@ public abstract partial class ImageFrameCollectionTests
using ImageFrame<Rgba32> addedFrame = this.Collection.AddFrame(Array.Empty<Rgba32>());
});
Assert.StartsWith($"Parameter \"data\" ({typeof(int)}) must be greater than or equal to {100}, was {0}", ex.Message);
Assert.StartsWith($"Parameter \"data\" ({typeof(long)}) must be greater than or equal to {100}, was {0}", ex.Message);
}
[Fact]

74
tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs

@ -52,5 +52,79 @@ public partial class ImageTests
Assert.Equal(Color.White, Color.FromPixel(img[1, 0]));
Assert.Equal(Color.Black, Color.FromPixel(img[1, 1]));
}
[Fact]
public void FromPixels_WithRowStride()
{
Rgba32[] data =
[
Color.Black.ToPixel<Rgba32>(),
Color.White.ToPixel<Rgba32>(),
Color.Red.ToPixel<Rgba32>(), // padding
Color.White.ToPixel<Rgba32>(),
Color.Black.ToPixel<Rgba32>(),
Color.Red.ToPixel<Rgba32>() // padding
];
using Image<Rgba32> img = Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStride: 3);
Assert.NotNull(img);
Assert.Equal(Color.Black, Color.FromPixel(img[0, 0]));
Assert.Equal(Color.White, Color.FromPixel(img[0, 1]));
Assert.Equal(Color.White, Color.FromPixel(img[1, 0]));
Assert.Equal(Color.Black, Color.FromPixel(img[1, 1]));
}
[Fact]
public void FromBytes_WithRowStrideInBytes()
{
byte[] data =
[
0, 0, 0, 255, // 0,0
255, 255, 255, 255, // 0,1
255, 0, 0, 255, // padding
255, 255, 255, 255, // 1,0
0, 0, 0, 255, // 1,1
255, 0, 0, 255 // padding
];
using Image<Rgba32> img = Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 12);
Assert.NotNull(img);
Assert.Equal(Color.Black, Color.FromPixel(img[0, 0]));
Assert.Equal(Color.White, Color.FromPixel(img[0, 1]));
Assert.Equal(Color.White, Color.FromPixel(img[1, 0]));
Assert.Equal(Color.Black, Color.FromPixel(img[1, 1]));
}
[Fact]
public void FromPixels_WithRowStride_InvalidStride_Throws()
{
Rgba32[] data = new Rgba32[6];
Assert.ThrowsAny<ArgumentOutOfRangeException>(
() => Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStride: 1));
}
[Fact]
public void FromPixels_WithRowStride_InvalidLength_Throws()
{
Rgba32[] data = new Rgba32[4];
Assert.ThrowsAny<ArgumentException>(
() => Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStride: 3));
}
[Fact]
public void FromBytes_WithRowStrideInBytes_InvalidStride_Throws()
{
byte[] data = new byte[24];
Assert.ThrowsAny<ArgumentException>(
() => Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 10));
}
[Fact]
public void FromBytes_WithRowStrideInBytes_InvalidLength_Throws()
{
byte[] data = new byte[19];
Assert.ThrowsAny<ArgumentException>(
() => Image.LoadPixelData<Rgba32>(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 12));
}
}
}

97
tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs

@ -141,6 +141,103 @@ public partial class ImageTests
}
}
[Fact]
public void WrapMemory_MemoryOfT_Strided_CreatedImageIsCorrect()
{
Rgba32[] source =
[
new Rgba32(1, 1, 1, 255),
new Rgba32(2, 2, 2, 255),
new Rgba32(3, 3, 3, 255),
new Rgba32(90, 90, 90, 255),
new Rgba32(4, 4, 4, 255),
new Rgba32(5, 5, 5, 255),
new Rgba32(6, 6, 6, 255),
new Rgba32(91, 91, 91, 255)
];
using Image<Rgba32> image = Image.WrapMemory(source.AsMemory(), width: 3, height: 2, rowStride: 4);
Assert.Equal(4, image.Frames.RootFrame.PixelBuffer.RowStride);
Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> _));
Assert.Equal(source[0], image[0, 0]);
Assert.Equal(source[2], image[2, 0]);
Assert.Equal(source[4], image[0, 1]);
Assert.Equal(source[6], image[2, 1]);
}
[Fact]
public void WrapMemory_MemoryOfT_Strided_CopyPixelDataTo_UsesRowStrideLayout()
{
Rgba32[] source =
[
new Rgba32(1, 1, 1, 255),
new Rgba32(2, 2, 2, 255),
new Rgba32(3, 3, 3, 255),
new Rgba32(90, 90, 90, 255),
new Rgba32(4, 4, 4, 255),
new Rgba32(5, 5, 5, 255),
new Rgba32(6, 6, 6, 255),
new Rgba32(91, 91, 91, 255)
];
using Image<Rgba32> image = Image.WrapMemory(source.AsMemory(), width: 3, height: 2, rowStride: 4);
Rgba32 sentinel = new(250, 1, 1, 255);
Rgba32[] destination = [sentinel, sentinel, sentinel, sentinel, sentinel, sentinel, sentinel];
image.CopyPixelDataTo(destination);
Assert.Equal(source[0], destination[0]);
Assert.Equal(source[1], destination[1]);
Assert.Equal(source[2], destination[2]);
Assert.Equal(sentinel, destination[3]);
Assert.Equal(source[4], destination[4]);
Assert.Equal(source[5], destination[5]);
Assert.Equal(source[6], destination[6]);
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => image.CopyPixelDataTo(new Rgba32[6]));
}
[Fact]
public void WrapMemory_MemoryOfByte_Strided_CreatedImageIsCorrect()
{
int pixelSize = Unsafe.SizeOf<Rgba32>();
byte[] sourceBytes = new byte[8 * pixelSize];
Span<Rgba32> source = MemoryMarshal.Cast<byte, Rgba32>(sourceBytes);
source[0] = new Rgba32(1, 1, 1, 255);
source[1] = new Rgba32(2, 2, 2, 255);
source[2] = new Rgba32(3, 3, 3, 255);
source[4] = new Rgba32(4, 4, 4, 255);
source[5] = new Rgba32(5, 5, 5, 255);
source[6] = new Rgba32(6, 6, 6, 255);
using Image<Rgba32> image = Image.WrapMemory<Rgba32>(
sourceBytes.AsMemory(),
width: 3,
height: 2,
rowStrideInBytes: 4 * pixelSize);
Assert.Equal(4, image.Frames.RootFrame.PixelBuffer.RowStride);
Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> _));
Assert.Equal(source[0], image[0, 0]);
Assert.Equal(source[2], image[2, 0]);
Assert.Equal(source[4], image[0, 1]);
Assert.Equal(source[6], image[2, 1]);
}
[Fact]
public void WrapMemory_Strided_InvalidStride_Throws()
{
Rgba32[] pixelSource = new Rgba32[8];
byte[] byteSource = new byte[8 * Unsafe.SizeOf<Rgba32>()];
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => Image.WrapMemory(pixelSource.AsMemory(), width: 3, height: 2, rowStride: 2));
Assert.ThrowsAny<ArgumentException>(() => Image.WrapMemory(pixelSource.AsMemory(0, 6), width: 3, height: 2, rowStride: 4));
Assert.ThrowsAny<ArgumentException>(() => Image.WrapMemory<Rgba32>(byteSource.AsMemory(), width: 3, height: 2, rowStrideInBytes: (4 * Unsafe.SizeOf<Rgba32>()) - 1));
Assert.ThrowsAny<ArgumentException>(() => Image.WrapMemory<Rgba32>(byteSource.AsMemory(0, 6 * Unsafe.SizeOf<Rgba32>()), width: 3, height: 2, rowStrideInBytes: 4 * Unsafe.SizeOf<Rgba32>()));
}
[Fact]
public void WrapSystemDrawingBitmap_WhenObserved()
{

43
tests/ImageSharp.Tests/Memory/Buffer2DTests.CopyTo.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Memory;
public partial class Buffer2DTests
{
[Fact]
public void CopyToSpan_StridedSource_WritesDestinationUsingSourceStride()
{
int[] source = [1, 2, 3, 777, 4, 5, 6, 888];
int[] destination = [-1, -1, -1, -1, -1, -1, -1];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(source.AsMemory(), width: 3, height: 2, stride: 4);
buffer.CopyTo(destination);
Assert.Equal(new[] { 1, 2, 3, -1, 4, 5, 6 }, destination);
}
[Fact]
public void CopyToSpan_PackedSource_WritesPackedDestination()
{
int[] source = [1, 2, 3, 4, 5, 6];
int[] destination = new int[6];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(source.AsMemory(), width: 3, height: 2);
buffer.CopyTo(destination);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6 }, destination);
}
[Fact]
public void CopyToSpan_StridedSource_ThrowsWhenDestinationTooShort()
{
int[] source = [1, 2, 3, 777, 4, 5, 6, 888];
int[] destination = new int[6];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(source.AsMemory(), width: 3, height: 2, stride: 4);
Assert.Throws<ArgumentOutOfRangeException>(() => buffer.CopyTo(destination));
}
}

69
tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs

@ -152,5 +152,74 @@ public partial class Buffer2DTests
Assert.Equal(color, source.MemoryGroup[0].Span[10]);
Assert.NotEqual(color, dest.MemoryGroup[0].Span[10]);
}
[Fact]
public void WhenDestIsNotMemoryOwner_DifferentSizeSameTotal_PackedLayout_ShouldCopy()
{
int[] data = new int[6];
using TestMemoryManager<int> destOwner = new(data);
using Buffer2D<int> dest = new(MemoryGroup<int>.Wrap(destOwner.Memory), 2, 3);
using Buffer2D<int> source = this.memoryAllocator.Allocate2D<int>(3, 2, AllocationOptions.Clean);
source[0, 0] = 1;
source[1, 0] = 2;
source[2, 0] = 3;
source[0, 1] = 4;
source[1, 1] = 5;
source[2, 1] = 6;
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(3, 2), dest.Size());
Assert.Equal(6, dest[2, 1]);
}
[Fact]
public void WhenDestIsNotMemoryOwner_DifferentSizeSameTotal_StridedLayout_ShouldCopy()
{
int[] data = new int[5];
using Buffer2D<int> dest = Buffer2D<int>.WrapMemory(data.AsMemory(), width: 2, height: 2, stride: 3);
using Buffer2D<int> source = this.memoryAllocator.Allocate2D<int>(1, 5, AllocationOptions.Clean);
source[0, 0] = 1;
source[0, 1] = 2;
source[0, 2] = 3;
source[0, 3] = 4;
source[0, 4] = 5;
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(1, 5), dest.Size());
Assert.Equal(1, dest[0, 0]);
Assert.Equal(2, dest[0, 1]);
Assert.Equal(3, dest[0, 2]);
Assert.Equal(4, dest[0, 3]);
Assert.Equal(5, dest[0, 4]);
}
[Fact]
public void WhenDestIsNotMemoryOwner_DifferentSizeDifferentTotal_ButBothLayoutsFit_ShouldCopy()
{
int[] data = new int[5];
using TestMemoryManager<int> destOwner = new(data);
using Buffer2D<int> dest = new(MemoryGroup<int>.Wrap(destOwner.Memory), 1, 3);
using Buffer2D<int> source = this.memoryAllocator.Allocate2D<int>(2, 2, AllocationOptions.Clean);
source[0, 0] = 1;
source[1, 0] = 2;
source[0, 1] = 3;
source[1, 1] = 4;
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(2, 2), dest.Size());
Assert.Equal(1, dest[0, 0]);
Assert.Equal(2, dest[1, 0]);
Assert.Equal(3, dest[0, 1]);
Assert.Equal(4, dest[1, 1]);
}
}
}

91
tests/ImageSharp.Tests/Memory/Buffer2DTests.WrapMemory.cs

@ -0,0 +1,91 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Memory;
public partial class Buffer2DTests
{
[Fact]
public void WrapMemory_Packed_DangerousTryGetSinglePixelMemory_ReturnsTrue()
{
int[] data = [1, 2, 3, 4, 5, 6];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 3);
Assert.True(buffer.DangerousTryGetSingleMemory(out Memory<int> memory));
Assert.Equal(3, buffer.RowStride);
Assert.Equal(6, memory.Length);
Assert.True(Unsafe.AreSame(ref data[0], ref memory.Span[0]));
Assert.SpanPointsTo(buffer.DangerousGetRowSpan(1), data.AsMemory(), bufferOffset: 3);
}
[Fact]
public void WrapMemory_Strided_DangerousTryGetSinglePixelMemory_ReturnsFalse()
{
int[] data = [1, 2, 3, 777, 4, 5, 6, 888];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 4);
Assert.False(buffer.DangerousTryGetSingleMemory(out Memory<int> _));
Assert.Equal(4, buffer.RowStride);
Assert.Equal(4, new Buffer2DRegion<int>(buffer).Stride);
Span<int> row0 = buffer.DangerousGetRowSpan(0);
Span<int> row1 = buffer.DangerousGetRowSpan(1);
Assert.Equal(3, row0.Length);
Assert.Equal(3, row1.Length);
Assert.Equal(1, row0[0]);
Assert.Equal(3, row0[2]);
Assert.Equal(4, row1[0]);
Assert.Equal(6, row1[2]);
Assert.SpanPointsTo(row0, data.AsMemory(), bufferOffset: 0);
Assert.SpanPointsTo(row1, data.AsMemory(), bufferOffset: 4);
}
[Fact]
public void WrapMemory_Packed_WithTrailingData_DangerousTryGetSinglePixelMemory_IsLogicalSize()
{
int[] data = [1, 2, 3, 4, 5, 6, 777, 888];
using Buffer2D<int> buffer = Buffer2D<int>.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 3);
Assert.True(buffer.DangerousTryGetSingleMemory(out Memory<int> memory));
Assert.Equal(6, memory.Length);
Assert.True(Unsafe.AreSame(ref data[0], ref memory.Span[0]));
}
[Fact]
public void WrapMemory_Strided_ThrowsWhenStrideIsLessThanWidth()
{
int[] data = new int[10];
Assert.Throws<ArgumentOutOfRangeException>(() => Buffer2D<int>.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 2));
}
[Fact]
public void WrapMemory_Strided_ThrowsWhenInputMemoryTooSmall()
{
int[] data = new int[6];
Assert.Throws<ArgumentException>(() => Buffer2D<int>.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 4));
}
[Theory]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(0, 0)]
[InlineData(-1, 1)]
[InlineData(1, -1)]
public void WrapMemory_ThrowsWhenWidthOrHeightIsNotPositive(int width, int height)
{
int[] data = new int[16];
Assert.Throws<ArgumentOutOfRangeException>(() => Buffer2D<int>.WrapMemory(data.AsMemory(), width, height));
Assert.Throws<ArgumentOutOfRangeException>(() => Buffer2D<int>.WrapMemory(data.AsMemory(), width, height, stride: 4));
}
}

46
tests/ImageSharp.Tests/Memory/Buffer2DTests.cs

@ -50,17 +50,11 @@ public partial class Buffer2DTests
[InlineData(Big, 1, 0)]
[InlineData(60, 42, 0)]
[InlineData(3, 0, 0)]
public unsafe void Construct_Empty(int bufferCapacity, int width, int height)
public unsafe void Construct_Empty_Throws(int bufferCapacity, int width, int height)
{
this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity;
using (Buffer2D<TestStructs.Foo> buffer = this.MemoryAllocator.Allocate2D<TestStructs.Foo>(width, height))
{
Assert.Equal(width, buffer.Width);
Assert.Equal(height, buffer.Height);
Assert.Equal(0, buffer.FastMemoryGroup.TotalLength);
Assert.Equal(0, buffer.DangerousGetSingleSpan().Length);
}
Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.Allocate2D<TestStructs.Foo>(width, height));
}
[Theory]
@ -339,25 +333,47 @@ public partial class Buffer2DTests
Assert.NotSame(mgBefore, buffer1.MemoryGroup);
}
public static TheoryData<Size> InvalidLengths { get; set; } = new()
public static TheoryData<Size> InvalidDimensions { get; set; } = new()
{
{ new Size(-1, -1) },
{ new Size(0, 1) },
{ new Size(1, 0) },
{ new Size(0, 0) }
};
public static TheoryData<Size> OverflowDimensions { get; set; } = new()
{
{ new Size(32768, 32769) },
{ new Size(32769, 32768) }
};
[Theory]
[MemberData(nameof(InvalidLengths))]
public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(Size size)
[MemberData(nameof(InvalidDimensions))]
public void Allocate_InvalidDimensions_ThrowsArgumentOutOfRangeException(Size size)
=> Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.Allocate2D<Rgba32>(size.Width, size.Height));
[Theory]
[MemberData(nameof(InvalidDimensions))]
public void Allocate_InvalidDimensions_SizeOverload_ThrowsArgumentOutOfRangeException(Size size)
=> Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.Allocate2D<Rgba32>(new Size(size)));
[Theory]
[MemberData(nameof(InvalidDimensions))]
public void Allocate_InvalidDimensions_OverAligned_ThrowsArgumentOutOfRangeException(Size size)
=> Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.Allocate2DOveraligned<Rgba32>(size.Width, size.Height, 1));
[Theory]
[MemberData(nameof(OverflowDimensions))]
public void Allocate_OverflowDimensions_ThrowsInvalidMemoryOperationException(Size size)
=> Assert.Throws<InvalidMemoryOperationException>(() => this.MemoryAllocator.Allocate2D<Rgba32>(size.Width, size.Height));
[Theory]
[MemberData(nameof(InvalidLengths))]
public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_Size(Size size)
[MemberData(nameof(OverflowDimensions))]
public void Allocate_OverflowDimensions_SizeOverload_ThrowsInvalidMemoryOperationException(Size size)
=> Assert.Throws<InvalidMemoryOperationException>(() => this.MemoryAllocator.Allocate2D<Rgba32>(new Size(size)));
[Theory]
[MemberData(nameof(InvalidLengths))]
public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_OverAligned(Size size)
[MemberData(nameof(OverflowDimensions))]
public void Allocate_OverflowDimensions_OverAligned_ThrowsInvalidMemoryOperationException(Size size)
=> Assert.Throws<InvalidMemoryOperationException>(() => this.MemoryAllocator.Allocate2DOveraligned<Rgba32>(size.Width, size.Height, 1));
}

126
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs

@ -9,100 +9,74 @@ public partial class MemoryGroupTests
{
public class CopyTo : MemoryGroupTestsBase
{
public static readonly TheoryData<int, int, int, int> WhenSourceBufferIsShorterOrEqual_Data =
CopyAndTransformData;
[Theory]
[MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))]
public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen)
[Fact]
public void GroupToSpan_StridedSource_DoesNotRequireTrailingPadding()
{
using MemoryGroup<int> src = this.CreateTestGroup(srcTotal, srcBufLen, true);
using MemoryGroup<int> trg = this.CreateTestGroup(trgTotal, trgBufLen, false);
src.CopyTo(trg);
using MemoryGroup<int> src = this.CreateTestGroup(totalLength: 7, bufferLength: 4, fillSequence: true);
int[] trg = new int[6];
int pos = 0;
MemoryGroupIndex i = src.MinIndex();
MemoryGroupIndex j = trg.MinIndex();
for (; i < src.MaxIndex(); i += 1, j += 1, pos++)
{
int a = src.GetElementAt(i);
int b = trg.GetElementAt(j);
src.CopyTo(
sourceStride: 4,
trg,
targetStride: 3,
width: 3,
height: 2);
Assert.True(a == b, $"Mismatch @ {pos} Expected: {a} Actual: {b}");
}
Assert.Equal(new[] { 1, 2, 3, 5, 6, 7 }, trg);
}
[Fact]
public void WhenTargetBufferTooShort_Throws()
public void GroupToGroup_StridedCopy_DoesNotRequireTrailingPadding()
{
using MemoryGroup<int> src = this.CreateTestGroup(10, 20, true);
using MemoryGroup<int> trg = this.CreateTestGroup(5, 20, false);
Assert.Throws<ArgumentOutOfRangeException>(() => src.CopyTo(trg));
using MemoryGroup<int> src = this.CreateTestGroup(totalLength: 11, bufferLength: 5, fillSequence: true);
using MemoryGroup<int> trg = this.CreateTestGroup(totalLength: 13, bufferLength: 6, fillSequence: false);
src.CopyTo(
sourceStride: 4,
trg,
targetStride: 5,
width: 3,
height: 3);
Assert.Equal(1, GetElementAtLinearIndex(trg, 0));
Assert.Equal(2, GetElementAtLinearIndex(trg, 1));
Assert.Equal(3, GetElementAtLinearIndex(trg, 2));
Assert.Equal(5, GetElementAtLinearIndex(trg, 5));
Assert.Equal(6, GetElementAtLinearIndex(trg, 6));
Assert.Equal(7, GetElementAtLinearIndex(trg, 7));
Assert.Equal(9, GetElementAtLinearIndex(trg, 10));
Assert.Equal(10, GetElementAtLinearIndex(trg, 11));
Assert.Equal(11, GetElementAtLinearIndex(trg, 12));
}
[Theory]
[InlineData(30, 10, 40)]
[InlineData(42, 23, 42)]
[InlineData(1, 3, 10)]
[InlineData(0, 4, 0)]
public void GroupToSpan_Success(long totalLength, int bufferLength, int spanLength)
[Fact]
public void GroupToSpan_StridedSource_HeightOne()
{
using MemoryGroup<int> src = this.CreateTestGroup(totalLength, bufferLength, true);
int[] trg = new int[spanLength];
src.CopyTo(trg);
using MemoryGroup<int> src = this.CreateTestGroup(totalLength: 3, bufferLength: 2, fillSequence: true);
int[] trg = new int[3];
int expected = 1;
foreach (int val in trg.AsSpan().Slice(0, (int)totalLength))
{
Assert.Equal(expected, val);
expected++;
}
}
src.CopyTo(
sourceStride: 8,
trg,
targetStride: 3,
width: 3,
height: 1);
[Theory]
[InlineData(20, 7, 19)]
[InlineData(2, 1, 1)]
public void GroupToSpan_OutOfRange(long totalLength, int bufferLength, int spanLength)
{
using MemoryGroup<int> src = this.CreateTestGroup(totalLength, bufferLength, true);
int[] trg = new int[spanLength];
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => src.CopyTo(trg));
Assert.Equal(new[] { 1, 2, 3 }, trg);
}
[Theory]
[InlineData(30, 35, 10)]
[InlineData(42, 23, 42)]
[InlineData(10, 3, 1)]
[InlineData(0, 3, 0)]
public void SpanToGroup_Success(long totalLength, int bufferLength, int spanLength)
private static int GetElementAtLinearIndex(MemoryGroup<int> group, int index)
{
int[] src = new int[spanLength];
for (int i = 0; i < src.Length; i++)
{
src[i] = i + 1;
}
using MemoryGroup<int> trg = this.CreateTestGroup(totalLength, bufferLength);
src.AsSpan().CopyTo(trg);
int position = 0;
for (MemoryGroupIndex i = trg.MinIndex(); position < spanLength; i += 1, position++)
int pos = 0;
for (MemoryGroupIndex i = group.MinIndex(); i < group.MaxIndex(); i += 1, pos++)
{
int expected = position + 1;
Assert.Equal(expected, trg.GetElementAt(i));
if (pos == index)
{
return group.GetElementAt(i);
}
}
}
[Theory]
[InlineData(10, 3, 11)]
[InlineData(0, 3, 1)]
public void SpanToGroup_OutOfRange(long totalLength, int bufferLength, int spanLength)
{
int[] src = new int[spanLength];
using MemoryGroup<int> trg = this.CreateTestGroup(totalLength, bufferLength, true);
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => src.AsSpan().CopyTo(trg));
throw new ArgumentOutOfRangeException(nameof(index));
}
}
}

78
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

@ -26,76 +26,6 @@ public partial class MemoryGroupTests : MemoryGroupTestsBase
Assert.False(g.IsValid);
}
#pragma warning disable SA1509
private static readonly TheoryData<int, int, int, int> CopyAndTransformData =
new()
{
{ 20, 10, 20, 10 },
{ 20, 5, 20, 4 },
{ 20, 4, 20, 5 },
{ 18, 6, 20, 5 },
{ 19, 10, 20, 10 },
{ 21, 10, 22, 2 },
{ 1, 5, 5, 4 },
{ 30, 12, 40, 5 },
{ 30, 5, 40, 12 },
};
public class TransformTo : MemoryGroupTestsBase
{
public static readonly TheoryData<int, int, int, int> WhenSourceBufferIsShorterOrEqual_Data =
CopyAndTransformData;
[Theory]
[MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))]
public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen)
{
using MemoryGroup<int> src = this.CreateTestGroup(srcTotal, srcBufLen, true);
using MemoryGroup<int> trg = this.CreateTestGroup(trgTotal, trgBufLen, false);
src.TransformTo(trg, MultiplyAllBy2);
int pos = 0;
MemoryGroupIndex i = src.MinIndex();
MemoryGroupIndex j = trg.MinIndex();
for (; i < src.MaxIndex(); i += 1, j += 1, pos++)
{
int a = src.GetElementAt(i);
int b = trg.GetElementAt(j);
Assert.True(b == 2 * a, $"Mismatch @ {pos} Expected: {a} Actual: {b}");
}
}
[Fact]
public void WhenTargetBufferTooShort_Throws()
{
using MemoryGroup<int> src = this.CreateTestGroup(10, 20, true);
using MemoryGroup<int> trg = this.CreateTestGroup(5, 20, false);
Assert.Throws<ArgumentOutOfRangeException>(() => src.TransformTo(trg, MultiplyAllBy2));
}
}
[Theory]
[InlineData(100, 5)]
[InlineData(100, 101)]
public void TransformInplace(int totalLength, int bufferLength)
{
using MemoryGroup<int> src = this.CreateTestGroup(10, 20, true);
src.TransformInplace(s => MultiplyAllBy2(s, s));
int cnt = 1;
for (MemoryGroupIndex i = src.MinIndex(); i < src.MaxIndex(); i += 1)
{
int val = src.GetElementAt(i);
Assert.Equal(expected: cnt * 2, val);
cnt++;
}
}
[Fact]
public void Wrap()
{
@ -224,12 +154,4 @@ public partial class MemoryGroupTests : MemoryGroupTestsBase
}
}
private static void MultiplyAllBy2(ReadOnlySpan<int> source, Span<int> target)
{
Assert.Equal(source.Length, target.Length);
for (int k = 0; k < source.Length; k++)
{
target[k] = source[k] * 2;
}
}
}

Loading…
Cancel
Save