// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; 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.Metadata; using SixLabors.ImageSharp.PixelFormats; using Xunit; // 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; var rectangle = new System.Drawing.Rectangle(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); } 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() { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); var array = new Rgba32[25]; var memory = new Memory(array); using (var image = Image.WrapMemory(cfg, memory, 5, 5, metaData)) { Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); ref Rgba32 pixel0 = ref imageSpan[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(metaData, image.Metadata); } } [Fact] public void WrapSystemDrawingBitmap_WhenObserved() { if (ShouldSkipBitmapTest) { return; } using (var bmp = new Bitmap(51, 23)) { using (var memoryManager = new BitmapMemoryManager(bmp)) { Memory memory = memoryManager.Memory; Bgra32 bg = Color.Red; Bgra32 fg = Color.Green; using (var image = Image.WrapMemory(memory, bmp.Width, bmp.Height)) { Assert.Equal(memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory()); Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); imageSpan.Fill(bg); for (var i = 10; i < 20; i++) { image.GetPixelRowSpan(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); } } [Fact] public void WrapSystemDrawingBitmap_WhenOwned() { if (ShouldSkipBitmapTest) { return; } using (var bmp = new Bitmap(51, 23)) { var memoryManager = new BitmapMemoryManager(bmp); Bgra32 bg = Color.Red; Bgra32 fg = Color.Green; using (var image = Image.WrapMemory(memoryManager, bmp.Width, bmp.Height)) { Assert.Equal(memoryManager.Memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory()); Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); imageSpan.Fill(bg); for (var i = 10; i < 20; i++) { image.GetPixelRowSpan(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() { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); var array = new byte[25 * Unsafe.SizeOf()]; var memory = new Memory(array); using (var image = Image.WrapMemory(cfg, memory, 5, 5, metaData)) { Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); ref Rgba32 pixel0 = ref imageSpan[0]; Assert.True(Unsafe.AreSame(ref Unsafe.As(ref array[0]), ref pixel0)); Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(metaData, image.Metadata); } } [Fact] public void WrapSystemDrawingBitmap_FromBytes_WhenObserved() { if (ShouldSkipBitmapTest) { return; } using (var bmp = new Bitmap(51, 23)) { using (var memoryManager = new BitmapMemoryManager(bmp)) { Memory pixelMemory = memoryManager.Memory; Memory byteMemory = new CastMemoryManager(pixelMemory).Memory; Bgra32 bg = Color.Red; Bgra32 fg = Color.Green; using (var 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())); Assert.True(image.TryGetSinglePixelSpan(out imageSpan)); imageSpan.Fill(bg); for (var i = 10; i < 20; i++) { image.GetPixelRowSpan(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); } } [Fact] public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect() { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); var array = new Rgba32[25]; fixed (void* ptr = array) { using (var image = Image.WrapMemory(cfg, ptr, 5, 5, metaData)) { Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); ref Rgba32 pixel0 = ref imageSpan[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1)); Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(metaData, image.Metadata); } } } [Fact] public unsafe void WrapSystemDrawingBitmap_FromPointer() { if (ShouldSkipBitmapTest) { return; } using (var bmp = new Bitmap(51, 23)) { using (var memoryManager = new BitmapMemoryManager(bmp)) { Memory pixelMemory = memoryManager.Memory; Bgra32 bg = Color.Red; Bgra32 fg = Color.Green; fixed (void* p = pixelMemory.Span) { using (var image = Image.WrapMemory(p, 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())); Assert.True(image.TryGetSinglePixelSpan(out imageSpan)); imageSpan.Fill(bg); for (var i = 10; i < 20; i++) { image.GetPixelRowSpan(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)] public void WrapMemory_MemoryOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; var 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_MemoryOfT_ValidSize(int size, int height, int width) { var array = new Rgba32[size]; var memory = new Memory(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)] public void WrapMemory_IMemoryOwnerOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; var memory = new TestMemoryOwner { 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) { var array = new Rgba32[size]; var memory = new TestMemoryOwner { Memory = array }; using (var img = Image.WrapMemory(memory, width, height)) { Assert.Equal(width, img.Width); Assert.Equal(height, img.Height); for (int i = 0; i < height; ++i) { var arrayIndex = width * i; Span rowSpan = img.GetPixelRowSpan(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)] public void WrapMemory_IMemoryOwnerOfByte_InvalidSize(int size, int height, int width) { var array = new byte[size * Unsafe.SizeOf()]; var memory = new TestMemoryOwner { 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) { var pixelSize = Unsafe.SizeOf(); var array = new byte[size * pixelSize]; var memory = new TestMemoryOwner { Memory = array }; using (var img = Image.WrapMemory(memory, width, height)) { Assert.Equal(width, img.Width); Assert.Equal(height, img.Height); for (int i = 0; i < height; ++i) { var arrayIndex = pixelSize * width * i; Span rowSpan = img.GetPixelRowSpan(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)] public void WrapMemory_MemoryOfByte_InvalidSize(int size, int height, int width) { var array = new byte[size * Unsafe.SizeOf()]; var 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_MemoryOfByte_ValidSize(int size, int height, int width) { var array = new byte[size * Unsafe.SizeOf()]; var memory = new Memory(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, height, width)); } private static bool ShouldSkipBitmapTest => !TestEnvironment.Is64BitProcess || (TestHelpers.ImageSharpBuiltAgainst != "netcoreapp3.1" && TestHelpers.ImageSharpBuiltAgainst != "netcoreapp2.1"); } } }