From d5243b5e409666c5970b0380aff762111d500fc2 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 11 Jan 2024 11:53:00 +0100 Subject: [PATCH] Add `ReadOnlySpan` / `ReadOnlyMemory` overloads Implements: - `Source.NewFromMemory`, `Image.FindLoadBuffer` and `Image.NewFromBuffer` for `ReadOnlySpan`. - `Image.NewFromMemory` for `ReadOnlyMemory`. - `Image.NewFromMemoryCopy` for `ReadOnlySpan`. --- src/NetVips/Internal/Vips.cs | 26 ++++ src/NetVips/NetVips.csproj | 1 + src/NetVips/Source.cs | 3 +- src/NetVips/net6.0/Image.cs | 174 +++++++++++++++++++++++++ src/NetVips/net6.0/Source.cs | 48 +++++++ tests/NetVips.Tests/ConnectionTests.cs | 18 +++ tests/NetVips.Tests/ForeignTests.cs | 2 +- tests/NetVips.Tests/IoFuncsTests.cs | 32 ++++- 8 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 src/NetVips/net6.0/Image.cs create mode 100644 src/NetVips/net6.0/Source.cs diff --git a/src/NetVips/Internal/Vips.cs b/src/NetVips/Internal/Vips.cs index 10740bcf..a57b3a7f 100644 --- a/src/NetVips/Internal/Vips.cs +++ b/src/NetVips/Internal/Vips.cs @@ -295,6 +295,10 @@ internal static class VipsBlob [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_blob_get")] internal static extern IntPtr Get(VipsBlobManaged blob, out UIntPtr length); + + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_blob_copy")] + internal static extern unsafe IntPtr Copy(void* data, UIntPtr length); } internal static class VipsArea @@ -422,12 +426,24 @@ internal static class VipsImage internal static extern IntPtr NewFromMemory(IntPtr data, UIntPtr size, int width, int height, int bands, BandFormat format); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_image_new_from_memory")] + internal static extern unsafe IntPtr NewFromMemory(void* data, UIntPtr size, int width, int height, + int bands, BandFormat format); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_image_new_from_memory_copy")] internal static extern IntPtr NewFromMemoryCopy(IntPtr data, UIntPtr size, int width, int height, int bands, BandFormat format); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_image_new_from_memory_copy")] + internal static extern unsafe IntPtr NewFromMemoryCopy(void* data, UIntPtr size, int width, int height, + int bands, BandFormat format); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_image_new_matrix_from_array")] @@ -582,6 +598,11 @@ internal static class VipsForeign EntryPoint = "vips_foreign_find_load_buffer")] internal static extern IntPtr FindLoadBuffer(IntPtr data, ulong size); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_foreign_find_load_buffer")] + internal static extern unsafe IntPtr FindLoadBuffer(void* data, ulong size); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_foreign_find_load_source")] @@ -637,6 +658,11 @@ internal static class VipsSource EntryPoint = "vips_source_new_from_file")] internal static extern IntPtr NewFromFile(byte[] filename); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_source_new_from_blob")] + internal static extern IntPtr NewFromBlob(VipsBlobManaged blob); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_source_new_from_memory")] diff --git a/src/NetVips/NetVips.csproj b/src/NetVips/NetVips.csproj index d2c763e9..1a46cd08 100644 --- a/src/NetVips/NetVips.csproj +++ b/src/NetVips/NetVips.csproj @@ -11,6 +11,7 @@ x64;x86;ARM64;ARM32 true + true false true true diff --git a/src/NetVips/Source.cs b/src/NetVips/Source.cs index 7695020f..c6c94d67 100644 --- a/src/NetVips/Source.cs +++ b/src/NetVips/Source.cs @@ -7,7 +7,7 @@ namespace NetVips /// /// An input connection. /// - public class Source : Connection + public partial class Source : Connection { // private static Logger logger = LogManager.GetCurrentClassLogger(); @@ -96,7 +96,6 @@ public static Source NewFromMemory(byte[] data) var handle = GCHandle.Alloc(data, GCHandleType.Pinned); var pointer = Internal.VipsSource.NewFromMemory(handle.AddrOfPinnedObject(), (UIntPtr)data.Length); - if (pointer == IntPtr.Zero) { if (handle.IsAllocated) diff --git a/src/NetVips/net6.0/Image.cs b/src/NetVips/net6.0/Image.cs new file mode 100644 index 00000000..92cd960b --- /dev/null +++ b/src/NetVips/net6.0/Image.cs @@ -0,0 +1,174 @@ +#if NET6_0_OR_GREATER + +namespace NetVips +{ + using System; + using System.Runtime.InteropServices; + using Internal; + + /// + /// Wrap a object. + /// + public partial class Image + { + #region helpers + + /// + /// Find the name of the load operation vips will use to load a buffer. + /// + /// + /// For example "VipsForeignLoadJpegBuffer". You can use this to work out what + /// options to pass to . + /// + /// The buffer to test. + /// Length of the buffer. + /// The name of the load operation, or . + internal static unsafe string FindLoadBuffer(void* data, ulong size) => + Marshal.PtrToStringAnsi(VipsForeign.FindLoadBuffer(data, size)); + + /// + /// Find the name of the load operation vips will use to load a buffer. + /// + /// + /// For example "VipsForeignLoadJpegBuffer". You can use this to work out what + /// options to pass to . + /// + /// The buffer to test. + /// The name of the load operation, or . + public static unsafe string FindLoadBuffer(ReadOnlySpan data) + { + fixed (byte* dataFixed = data) + { + return FindLoadBuffer(dataFixed, (ulong)data.Length); + } + } + + #endregion + + #region constructors + + /// + /// Load a formatted image from memory. + /// + /// + /// This behaves exactly as , but the image is + /// loaded from the memory object rather than from a file. The memory + /// object can be a string or buffer. + /// + /// The memory object to load the image from. + /// Load options as a string. Use for no options. + /// Hint the expected access pattern for the image. + /// The type of error that will cause load to fail. By + /// default, loaders are permissive, that is, . + /// Optional options that depend on the load operation. + /// A new . + /// If unable to load from . + public static unsafe Image NewFromBuffer( + ReadOnlySpan data, + string strOptions = "", + Enums.Access? access = null, + Enums.FailOn? failOn = null, + VOption kwargs = null) + { + fixed (byte* dataFixed = data) + { + var operationName = FindLoadBuffer(dataFixed, (ulong)data.Length); + if (operationName == null) + { + throw new VipsException("unable to load from buffer"); + } + + var options = new VOption(); + if (kwargs != null) + { + options.Merge(kwargs); + } + + options.AddIfPresent(nameof(access), access); + options.AddFailOn(failOn); + + options.Add("string_options", strOptions); + + var ptr = Internal.VipsBlob.Copy(dataFixed, (UIntPtr)data.Length); + if (ptr == IntPtr.Zero) + { + throw new VipsException("unable to load from buffer"); + } + + using var blob = new VipsBlob(ptr); + return Operation.Call(operationName, options, blob) as Image; + } + } + + /// + /// Wrap an image around a memory array. + /// + /// A . + /// Image width in pixels. + /// Image height in pixels. + /// Number of bands. + /// Band format. + /// A new . + /// If unable to make image from . + public static unsafe Image NewFromMemory( + ReadOnlyMemory data, + int width, + int height, + int bands, + Enums.BandFormat format) where T : unmanaged + { + var handle = data.Pin(); + var vi = VipsImage.NewFromMemory(handle.Pointer, (UIntPtr)data.Length, width, height, bands, + format); + + if (vi == IntPtr.Zero) + { + handle.Dispose(); + + throw new VipsException("unable to make image from memory"); + } + + var image = new Image(vi) { MemoryPressure = data.Length }; + + // Need to release the pinned MemoryHandle when the image is closed. + image.OnPostClose += () => handle.Dispose(); + + return image; + } + + /// + /// Like , but + /// for , so we must copy as it could be allocated on the stack. + /// + /// A . + /// Image width in pixels. + /// Image height in pixels. + /// Number of bands. + /// Band format. + /// A new . + /// If unable to make image from . + public static unsafe Image NewFromMemoryCopy( + ReadOnlySpan data, + int width, + int height, + int bands, + Enums.BandFormat format) where T : unmanaged + { + fixed (T* dataFixed = data) + { + var vi = VipsImage.NewFromMemoryCopy(dataFixed, (UIntPtr)data.Length, width, height, bands, format); + + if (vi == IntPtr.Zero) + { + throw new VipsException("unable to make image from memory"); + } + + return new Image(vi) { MemoryPressure = data.Length }; + } + } + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/src/NetVips/net6.0/Source.cs b/src/NetVips/net6.0/Source.cs new file mode 100644 index 00000000..77cf3949 --- /dev/null +++ b/src/NetVips/net6.0/Source.cs @@ -0,0 +1,48 @@ +#if NET6_0_OR_GREATER + +namespace NetVips +{ + using System; + + /// + /// An input connection. + /// + public partial class Source + { + /// + /// Make a new source from a memory object. + /// + /// + /// Make a new source that is attached to the memory object. For example: + /// + /// using var source = Source.NewFromMemory(data); + /// + /// You can pass this source to (for example) . + /// + /// The memory object. + /// A new . + /// If unable to create a new from . + public static unsafe Source NewFromMemory(ReadOnlySpan data) + { + fixed (byte* dataFixed = data) + { + var ptr = Internal.VipsBlob.Copy(dataFixed, (UIntPtr)data.Length); + if (ptr == IntPtr.Zero) + { + throw new VipsException("can't create input source from memory"); + } + + using var blob = new VipsBlob(ptr); + var pointer = Internal.VipsSource.NewFromBlob(blob); + if (pointer == IntPtr.Zero) + { + throw new VipsException("can't create input source from memory"); + } + + return new Source(pointer); + } + } + } +} + +#endif \ No newline at end of file diff --git a/tests/NetVips.Tests/ConnectionTests.cs b/tests/NetVips.Tests/ConnectionTests.cs index d87a461a..9e8e873d 100644 --- a/tests/NetVips.Tests/ConnectionTests.cs +++ b/tests/NetVips.Tests/ConnectionTests.cs @@ -2,6 +2,7 @@ namespace NetVips.Tests { using System; using System.IO; + using System.Text; using Xunit; using Xunit.Abstractions; @@ -49,6 +50,23 @@ public void TestConnection() Assert.True((image - image2).Abs().Max() < 10); } + [SkippableFact] + public void TestSourceNewFromMemorySpan() + { + Skip.IfNot(Helper.Have("svgload_source"), "no svg source support, skipping test"); + + ReadOnlySpan input = Encoding.UTF8 + .GetBytes("") + .AsSpan(); + var source = Source.NewFromMemory(input); + var image = Image.NewFromSource(source, access: Enums.Access.Sequential); + var image2 = Image.NewFromBuffer(input, access: Enums.Access.Sequential); + + Assert.Equal(0, (image - image2).Abs().Max()); + Assert.Equal(200, image.Width); + Assert.Equal(200, image.Height); + } + [SkippableFact] public void TestSourceCustomNoSeek() { diff --git a/tests/NetVips.Tests/ForeignTests.cs b/tests/NetVips.Tests/ForeignTests.cs index 0eb14c3e..658574bf 100644 --- a/tests/NetVips.Tests/ForeignTests.cs +++ b/tests/NetVips.Tests/ForeignTests.cs @@ -390,7 +390,7 @@ public void TestBufferOverload() Assert.Equal(_colour.Width, x.Width); Assert.Equal(_colour.Height, x.Height); Assert.Equal(_colour.Bands, x.Bands); - Assert.True((_colour - x).Abs().Max() <= 0); + Assert.Equal(0, (_colour - x).Abs().Max()); } [SkippableFact] diff --git a/tests/NetVips.Tests/IoFuncsTests.cs b/tests/NetVips.Tests/IoFuncsTests.cs index a7901f68..4d135892 100644 --- a/tests/NetVips.Tests/IoFuncsTests.cs +++ b/tests/NetVips.Tests/IoFuncsTests.cs @@ -107,7 +107,7 @@ public void TestNewFromImage() [Fact] public void TestNewFromMemory() { - var s = Enumerable.Repeat((byte)0, 200).ToArray(); + var s = new byte[200]; var im = Image.NewFromMemory(s, 20, 10, 1, Enums.BandFormat.Uchar); Assert.Equal(20, im.Width); Assert.Equal(10, im.Height); @@ -143,6 +143,36 @@ public void TestNewFromMemoryPtr() Cache.Max = prevMax; } + [Fact] + public void TestNewFromMemoryReadOnly() + { + var s = new ReadOnlyMemory(new byte[200]); + var im = Image.NewFromMemory(s, 20, 10, 1, Enums.BandFormat.Uchar); + Assert.Equal(20, im.Width); + Assert.Equal(10, im.Height); + Assert.Equal(Enums.BandFormat.Uchar, im.Format); + Assert.Equal(1, im.Bands); + Assert.Equal(0, im.Avg()); + + im += 10; + Assert.Equal(10, im.Avg()); + } + + [Fact] + public void TestNewFromMemoryCopySpan() + { + ReadOnlySpan s = stackalloc byte[200]; + var im = Image.NewFromMemoryCopy(s, 20, 10, 1, Enums.BandFormat.Uchar); + Assert.Equal(20, im.Width); + Assert.Equal(10, im.Height); + Assert.Equal(Enums.BandFormat.Uchar, im.Format); + Assert.Equal(1, im.Bands); + Assert.Equal(0, im.Avg()); + + im += 10; + Assert.Equal(10, im.Avg()); + } + [SkippableFact] public void TestGetFields() {