// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests;
public partial class ImageTests
{
public class WrapMemory
{
///
/// A exposing the locked pixel memory of a instance.
/// TODO: This should be an example in https://github.com/SixLabors/Samples
///
public class BitmapMemoryManager : MemoryManager
{
private readonly Bitmap bitmap;
private readonly BitmapData bmpData;
private readonly int length;
public BitmapMemoryManager(Bitmap bitmap)
{
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
{
throw new ArgumentException("bitmap.PixelFormat != PixelFormat.Format32bppArgb", nameof(bitmap));
}
this.bitmap = bitmap;
System.Drawing.Rectangle rectangle = new(0, 0, bitmap.Width, bitmap.Height);
this.bmpData = bitmap.LockBits(rectangle, ImageLockMode.ReadWrite, bitmap.PixelFormat);
this.length = bitmap.Width * bitmap.Height;
}
public bool IsDisposed { get; private set; }
protected override void Dispose(bool disposing)
{
if (this.IsDisposed)
{
return;
}
if (disposing)
{
this.bitmap.UnlockBits(this.bmpData);
}
this.IsDisposed = true;
}
public override unsafe Span GetSpan()
{
void* ptr = (void*)this.bmpData.Scan0;
return new Span(ptr, this.length);
}
public override unsafe MemoryHandle Pin(int elementIndex = 0)
{
void* ptr = (void*)this.bmpData.Scan0;
return new MemoryHandle(ptr, pinnable: this);
}
public override void Unpin()
{
}
}
public sealed class CastMemoryManager : MemoryManager
where TFrom : unmanaged
where TTo : unmanaged
{
private readonly Memory memory;
public CastMemoryManager(Memory memory)
{
this.memory = memory;
}
///
protected override void Dispose(bool disposing)
{
}
///
public override Span GetSpan()
{
return MemoryMarshal.Cast(this.memory.Span);
}
///
public override MemoryHandle Pin(int elementIndex = 0)
{
int byteOffset = elementIndex * Unsafe.SizeOf();
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf(), out int remainder);
if (remainder != 0)
{
ThrowHelper.ThrowArgumentException("The input index doesn't result in an aligned item access",
nameof(elementIndex));
}
return this.memory.Slice(shiftedOffset).Pin();
}
///
public override void Unpin()
{
}
}
[Fact]
public void WrapMemory_CreatedImageIsCorrect()
{
Configuration cfg = Configuration.CreateDefaultInstance();
ImageMetadata metaData = new();
Rgba32[] array = new Rgba32[25];
Memory memory = new(array);
using (Image image = Image.WrapMemory(cfg, memory, 5, 5, metaData))
{
Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem));
ref Rgba32 pixel0 = ref imageMem.Span[0];
Assert.True(Unsafe.AreSame(ref array[0], ref pixel0));
Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata);
}
}
[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 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 _));
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 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(() => image.CopyPixelDataTo(new Rgba32[6]));
}
[Fact]
public void WrapMemory_MemoryOfByte_Strided_CreatedImageIsCorrect()
{
int pixelSize = Unsafe.SizeOf();
byte[] sourceBytes = new byte[8 * pixelSize];
Span source = MemoryMarshal.Cast(sourceBytes.AsSpan());
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 image = Image.WrapMemory(
sourceBytes.AsMemory(),
width: 3,
height: 2,
rowStrideInBytes: 4 * pixelSize);
Assert.Equal(4, image.Frames.RootFrame.PixelBuffer.RowStride);
Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory _));
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()];
Assert.ThrowsAny(() => Image.WrapMemory(pixelSource.AsMemory(), width: 3, height: 2, rowStride: 2));
Assert.ThrowsAny(() => Image.WrapMemory(pixelSource.AsMemory(0, 6), width: 3, height: 2, rowStride: 4));
Assert.ThrowsAny(() => Image.WrapMemory(byteSource.AsMemory(), width: 3, height: 2, rowStrideInBytes: (4 * Unsafe.SizeOf()) - 1));
Assert.ThrowsAny(() => Image.WrapMemory(byteSource.AsMemory(0, 6 * Unsafe.SizeOf()), width: 3, height: 2, rowStrideInBytes: 4 * Unsafe.SizeOf()));
}
[Fact]
public void WrapSystemDrawingBitmap_WhenObserved()
{
if (ShouldSkipBitmapTest)
{
return;
}
using (Bitmap bmp = new(51, 23))
{
using (BitmapMemoryManager memoryManager = new(bmp))
{
Memory memory = memoryManager.Memory;
Bgra32 bg = Color.Red.ToPixel();
Bgra32 fg = Color.Green.ToPixel();
using (Image image = Image.WrapMemory(memory, bmp.Width, bmp.Height))
{
Assert.Equal(memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory());
image.GetPixelMemoryGroup().Fill(bg);
image.ProcessPixelRows(accessor =>
{
for (int i = 10; i < 20; i++)
{
accessor.GetRowSpan(i).Slice(10, 10).Fill(fg);
}
});
}
Assert.False(memoryManager.IsDisposed);
}
if (!Directory.Exists(TestEnvironment.ActualOutputDirectoryFullPath))
{
Directory.CreateDirectory(TestEnvironment.ActualOutputDirectoryFullPath);
}
string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp");
bmp.Save(fn, ImageFormat.Bmp);
}
}
[Fact]
public void WrapSystemDrawingBitmap_WhenOwned()
{
if (ShouldSkipBitmapTest)
{
return;
}
using (Bitmap bmp = new(51, 23))
{
BitmapMemoryManager memoryManager = new(bmp);
Bgra32 bg = Color.Red.ToPixel();
Bgra32 fg = Color.Green.ToPixel();
using (Image image = Image.WrapMemory(memoryManager, bmp.Width, bmp.Height))
{
Assert.Equal(memoryManager.Memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory());
image.GetPixelMemoryGroup().Fill(bg);
image.ProcessPixelRows(accessor =>
{
for (int i = 10; i < 20; i++)
{
accessor.GetRowSpan(i).Slice(10, 10).Fill(fg);
}
});
}
Assert.True(memoryManager.IsDisposed);
string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenOwned)}.bmp");
bmp.Save(fn, ImageFormat.Bmp);
}
}
[Fact]
public void WrapMemory_FromBytes_CreatedImageIsCorrect()
{
Configuration cfg = Configuration.CreateDefaultInstance();
ImageMetadata metaData = new();
byte[] array = new byte[25 * Unsafe.SizeOf()];
Memory memory = new(array);
using (Image image = Image.WrapMemory(cfg, memory, 5, 5, metaData))
{
Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem));
ref Rgba32 pixel0 = ref imageMem.Span[0];
Assert.True(Unsafe.AreSame(ref Unsafe.As(ref array[0]), ref pixel0));
Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata);
}
}
[Fact]
public void WrapSystemDrawingBitmap_FromBytes_WhenObserved()
{
if (ShouldSkipBitmapTest)
{
return;
}
using (Bitmap bmp = new(51, 23))
{
using (BitmapMemoryManager memoryManager = new(bmp))
{
Memory pixelMemory = memoryManager.Memory;
Memory byteMemory = new CastMemoryManager(pixelMemory).Memory;
Bgra32 bg = Color.Red.ToPixel();
Bgra32 fg = Color.Green.ToPixel();
using (Image image = Image.WrapMemory(byteMemory, bmp.Width, bmp.Height))
{
Span pixelSpan = pixelMemory.Span;
Span imageSpan = image.GetRootFramePixelBuffer().DangerousGetSingleMemory().Span;
// We can't compare the two Memory instances directly as they wrap different memory managers.
// To check that the underlying data matches, we can just manually check their lenth, and the
// fact that a reference to the first pixel in both spans is actually the same memory location.
Assert.Equal(pixelSpan.Length, imageSpan.Length);
Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(),
ref imageSpan.GetPinnableReference()));
image.GetPixelMemoryGroup().Fill(bg);
image.ProcessPixelRows(accessor =>
{
for (int i = 10; i < 20; i++)
{
accessor.GetRowSpan(i).Slice(10, 10).Fill(fg);
}
});
}
Assert.False(memoryManager.IsDisposed);
}
string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp");
bmp.Save(fn, ImageFormat.Bmp);
}
}
[Theory]
[InlineData(20, 5, 5)]
[InlineData(1023, 32, 32)]
[InlineData(65536, 65537, 65536)]
public unsafe void WrapMemory_Throws_OnTooLessWrongSize(int size, int width, int height)
{
Configuration cfg = Configuration.CreateDefaultInstance();
ImageMetadata metaData = new();
Rgba32[] array = new Rgba32[size];
Exception thrownException = null;
fixed (void* ptr = array)
{
try
{
using Image image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData);
}
catch (Exception e)
{
thrownException = e;
}
}
Assert.IsType(thrownException);
}
[Theory]
[InlineData(25, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1024, 32, 32)]
[InlineData(2048, 32, 32)]
public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect(int size, int width, int height)
{
Configuration cfg = Configuration.CreateDefaultInstance();
ImageMetadata metaData = new();
Rgba32[] array = new Rgba32[size];
fixed (void* ptr = array)
{
using (Image image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData))
{
Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem));
Span imageSpan = imageMem.Span;
Span sourceSpan = array.AsSpan(0, width * height);
ref Rgba32 pixel0 = ref imageSpan[0];
Assert.True(Unsafe.AreSame(ref sourceSpan[0], ref pixel0));
ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1];
Assert.True(Unsafe.AreSame(ref sourceSpan[sourceSpan.Length - 1], ref pixel_1));
Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata);
}
}
}
[Fact]
public unsafe void WrapSystemDrawingBitmap_FromPointer()
{
if (ShouldSkipBitmapTest)
{
return;
}
using (Bitmap bmp = new(51, 23))
{
using (BitmapMemoryManager memoryManager = new(bmp))
{
Memory pixelMemory = memoryManager.Memory;
Bgra32 bg = Color.Red.ToPixel();
Bgra32 fg = Color.Green.ToPixel();
fixed (void* p = pixelMemory.Span)
{
using (Image image = Image.WrapMemory(p, pixelMemory.Length, bmp.Width, bmp.Height))
{
Span pixelSpan = pixelMemory.Span;
Span imageSpan = image.GetRootFramePixelBuffer().DangerousGetSingleMemory().Span;
Assert.Equal(pixelSpan.Length, imageSpan.Length);
Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(),
ref imageSpan.GetPinnableReference()));
image.GetPixelMemoryGroup().Fill(bg);
image.ProcessPixelRows(accessor =>
{
for (int i = 10; i < 20; i++)
{
accessor.GetRowSpan(i).Slice(10, 10).Fill(fg);
}
});
}
Assert.False(memoryManager.IsDisposed);
}
}
string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp");
bmp.Save(fn, ImageFormat.Bmp);
}
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(1023, 32, 32)]
[InlineData(65536, 65537, 65536)]
public void WrapMemory_MemoryOfT_InvalidSize(int size, int height, int width)
{
Rgba32[] array = new Rgba32[size];
Memory memory = new(array);
Assert.Throws(() => Image.WrapMemory(memory, height, width));
}
[Theory]
[InlineData(25, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1024, 32, 32)]
[InlineData(2048, 32, 32)]
public void WrapMemory_MemoryOfT_ValidSize(int size, int height, int width)
{
Rgba32[] array = new Rgba32[size];
Memory memory = new(array);
Image.WrapMemory(memory, height, width);
}
private class TestMemoryOwner : IMemoryOwner
{
public bool Disposed { get; private set; }
public Memory Memory { get; set; }
public void Dispose() => this.Disposed = true;
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(1023, 32, 32)]
[InlineData(65536, 65537, 65536)]
public void WrapMemory_IMemoryOwnerOfT_InvalidSize(int size, int height, int width)
{
Rgba32[] array = new Rgba32[size];
TestMemoryOwner memory = new() { Memory = array };
Assert.Throws(() => Image.WrapMemory(memory, height, width));
}
[Theory]
[InlineData(25, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1024, 32, 32)]
[InlineData(2048, 32, 32)]
public void WrapMemory_IMemoryOwnerOfT_ValidSize(int size, int height, int width)
{
Rgba32[] array = new Rgba32[size];
TestMemoryOwner memory = new() { Memory = array };
using (Image img = Image.WrapMemory(memory, width, height))
{
Assert.Equal(width, img.Width);
Assert.Equal(height, img.Height);
img.ProcessPixelRows(accessor =>
{
for (int i = 0; i < height; ++i)
{
int arrayIndex = width * i;
Span rowSpan = accessor.GetRowSpan(i);
ref Rgba32 r0 = ref rowSpan[0];
ref Rgba32 r1 = ref array[arrayIndex];
Assert.True(Unsafe.AreSame(ref r0, ref r1));
}
});
}
Assert.True(memory.Disposed);
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(1023, 32, 32)]
[InlineData(65536, 65537, 65536)]
public void WrapMemory_IMemoryOwnerOfByte_InvalidSize(int size, int height, int width)
{
byte[] array = new byte[size * Unsafe.SizeOf()];
TestMemoryOwner memory = new() { Memory = array };
Assert.Throws(() => Image.WrapMemory(memory, height, width));
}
[Theory]
[InlineData(25, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1024, 32, 32)]
[InlineData(2048, 32, 32)]
public void WrapMemory_IMemoryOwnerOfByte_ValidSize(int size, int height, int width)
{
int pixelSize = Unsafe.SizeOf();
byte[] array = new byte[size * pixelSize];
TestMemoryOwner memory = new() { Memory = array };
using (Image img = Image.WrapMemory(memory, width, height))
{
Assert.Equal(width, img.Width);
Assert.Equal(height, img.Height);
img.ProcessPixelRows(acccessor =>
{
for (int i = 0; i < height; ++i)
{
int arrayIndex = pixelSize * width * i;
Span rowSpan = acccessor.GetRowSpan(i);
ref Rgba32 r0 = ref rowSpan[0];
ref Rgba32 r1 = ref Unsafe.As(ref array[arrayIndex]);
Assert.True(Unsafe.AreSame(ref r0, ref r1));
}
});
}
Assert.True(memory.Disposed);
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(1023, 32, 32)]
[InlineData(65536, 65537, 65536)]
public void WrapMemory_MemoryOfByte_InvalidSize(int size, int height, int width)
{
byte[] array = new byte[size * Unsafe.SizeOf()];
Memory memory = new(array);
Assert.Throws(() => Image.WrapMemory(memory, height, width));
}
[Theory]
[InlineData(25, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1024, 32, 32)]
[InlineData(2048, 32, 32)]
public void WrapMemory_MemoryOfByte_ValidSize(int size, int height, int width)
{
byte[] array = new byte[size * Unsafe.SizeOf()];
Memory memory = new(array);
Image.WrapMemory(memory, height, width);
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1023, 32, 32)]
public unsafe void WrapMemory_Pointer_Null(int size, int height, int width)
{
Assert.Throws(() => Image.WrapMemory((void*)null, size, height, width));
}
private static bool ShouldSkipBitmapTest =>
!TestEnvironment.Is64BitProcess || (TestHelpers.ImageSharpBuiltAgainst != "netcoreapp3.1" &&
TestHelpers.ImageSharpBuiltAgainst != "netcoreapp2.1");
}
}