diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index 2d3c29ed42..d89c44dc56 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -17,7 +17,7 @@ public abstract partial class Image { /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// /// The pixel type /// The @@ -38,6 +38,7 @@ public static Image WrapMemory( { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); + Guard.IsTrue(pixelMemory.Length == width * height, nameof(pixelMemory), "The length of the input memory doesn't match the specified image size"); var memorySource = MemoryGroup.Wrap(pixelMemory); return new Image(configuration, memorySource, width, height, metadata); @@ -45,7 +46,7 @@ public static Image WrapMemory( /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// /// The pixel type /// The @@ -64,7 +65,7 @@ public static Image WrapMemory( /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// The memory is being observed, the caller remains responsible for managing it's lifecycle. /// /// The pixel type. @@ -81,7 +82,7 @@ public static Image WrapMemory( /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// The ownership of the is being transferred to the new instance, /// meaning that the caller is not allowed to dispose . /// It will be disposed together with the result image. @@ -105,6 +106,7 @@ public static Image WrapMemory( { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); + Guard.IsTrue(pixelMemoryOwner.Memory.Length == width * height, nameof(pixelMemoryOwner), "The length of the input memory doesn't match the specified image size"); var memorySource = MemoryGroup.Wrap(pixelMemoryOwner); return new Image(configuration, memorySource, width, height, metadata); @@ -112,7 +114,7 @@ public static Image WrapMemory( /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// The ownership of the is being transferred to the new instance, /// meaning that the caller is not allowed to dispose . /// It will be disposed together with the result image. @@ -134,7 +136,7 @@ public static Image WrapMemory( /// /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, - /// allowing to view/manipulate it as an ImageSharp instance. + /// allowing to view/manipulate it as an instance. /// The ownership of the is being transferred to the new instance, /// meaning that the caller is not allowed to dispose . /// It will be disposed together with the result image. @@ -150,5 +152,73 @@ public static Image WrapMemory( int height) where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); + + /// + /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// + /// The pixel type + /// The + /// The byte memory representing the pixel data. + /// The width of the memory image. + /// The height of the memory image. + /// The . + /// The configuration is null. + /// The metadata is null. + /// An instance + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + var memoryManager = new ByteMemoryManager(byteMemory); + + Guard.IsTrue(memoryManager.Memory.Length == width * height, nameof(byteMemory), "The length of the input memory doesn't match the specified image size"); + + var memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, metadata); + } + + /// + /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// + /// The pixel type + /// The + /// The byte memory representing the pixel data. + /// The width of the memory image. + /// The height of the memory image. + /// The configuration is null. + /// An instance. + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemory, width, height, new ImageMetadata()); + + /// + /// Wraps an existing contiguous memory area of 'width' x 'height' pixels, + /// allowing to view/manipulate it as an instance. + /// The memory is being observed, the caller remains responsible for managing it's lifecycle. + /// + /// The pixel type. + /// The byte memory representing the pixel data. + /// The width of the memory image. + /// The height of the memory image. + /// An instance. + public static Image WrapMemory( + Memory byteMemory, + int width, + int height) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemory, width, height); } } diff --git a/src/ImageSharp/Memory/ByteMemoryManager{T}.cs b/src/ImageSharp/Memory/ByteMemoryManager{T}.cs new file mode 100644 index 0000000000..223709df65 --- /dev/null +++ b/src/ImageSharp/Memory/ByteMemoryManager{T}.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// A custom that can wrap of instances + /// and cast them to be for any arbitrary unmanaged value type. + /// + /// The value type to use when casting the wrapped instance. + internal sealed class ByteMemoryManager : MemoryManager + where T : unmanaged + { + /// + /// The wrapped of instance. + /// + private readonly Memory memory; + + /// + /// Initializes a new instance of the class. + /// + /// The of instance to wrap. + public ByteMemoryManager(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) + { + // We need to adjust the offset into the wrapped byte segment, + // as the input index refers to the target-cast memory of T. + // We just have to shift this index by the byte size of T. + return this.memory.Slice(elementIndex * Unsafe.SizeOf()).Pin(); + } + + /// + public override void Unpin() + { + } + } +} diff --git a/src/ImageSharp/Memory/MemoryOwnerExtensions.cs b/src/ImageSharp/Memory/MemoryOwnerExtensions.cs index aa475a80f1..c2551ccf2c 100644 --- a/src/ImageSharp/Memory/MemoryOwnerExtensions.cs +++ b/src/ImageSharp/Memory/MemoryOwnerExtensions.cs @@ -13,13 +13,27 @@ namespace SixLabors.ImageSharp.Memory /// internal static class MemoryOwnerExtensions { + /// + /// Gets a from an instance. + /// + /// The buffer + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span GetSpan(this IMemoryOwner buffer) - => buffer.Memory.Span; + { + return buffer.Memory.Span; + } + /// + /// Gets the length of an internal buffer. + /// + /// The buffer + /// The length of the buffer [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Length(this IMemoryOwner buffer) - => buffer.GetSpan().Length; + { + return buffer.Memory.Length; + } /// /// Gets a to an offsetted position inside the buffer. @@ -56,8 +70,16 @@ public static void Clear(this IMemoryOwner buffer) buffer.GetSpan().Clear(); } + /// + /// Gets a reference to the first item in the internal buffer for an instance. + /// + /// The buffer + /// A reference to the first item within the memory wrapped by + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T GetReference(this IMemoryOwner buffer) - where T : struct => - ref MemoryMarshal.GetReference(buffer.GetSpan()); + where T : struct + { + return ref MemoryMarshal.GetReference(buffer.GetSpan()); + } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index b11411e32a..448eb3833b 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -262,7 +262,7 @@ public PaletteDitherRowOperation( this.source = source; this.bounds = bounds; this.scale = processor.DitherScale; - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Length); } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 2b30d9459f..7dc7dbb30c 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -6,10 +6,10 @@ 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; using Xunit; @@ -80,10 +80,52 @@ 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.Default.Clone(); + var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); var array = new Rgba32[25]; @@ -173,6 +215,124 @@ public void WrapSystemDrawingBitmap_WhenOwned() } } + [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().GetSingleMemory().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); + } + } + + [Theory] + [InlineData(0, 5, 5)] + [InlineData(20, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [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)); + } + + private class TestMemoryOwner : IMemoryOwner + { + public Memory Memory { get; set; } + + public void Dispose() + { + } + } + + [Theory] + [InlineData(0, 5, 5)] + [InlineData(20, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [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(0, 5, 5)] + [InlineData(20, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [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)); + } + private static bool ShouldSkipBitmapTest => !TestEnvironment.Is64BitProcess || (TestHelpers.ImageSharpBuiltAgainst != "netcoreapp3.1" && TestHelpers.ImageSharpBuiltAgainst != "netcoreapp2.1"); }