From 089cf69fb9fae8077d89665cb2c179aafa585e64 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 (#225) Implements: - `Source.NewFromMemory`, `Image.FindLoadBuffer` and `Image.NewFromBuffer` for `ReadOnlySpan`. - `Image.NewFromMemory` for `ReadOnlyMemory`. - `Image.NewFromMemoryCopy` for `ReadOnlySpan`. --- CHANGELOG.md | 1 + src/NetVips/Internal/Vips.cs | 26 ++++ src/NetVips/NetVips.csproj | 1 + src/NetVips/Source.cs | 2 +- src/NetVips/net6.0/Image.cs | 171 +++++++++++++++++++++++++ src/NetVips/net6.0/Source.cs | 47 +++++++ tests/NetVips.Tests/ConnectionTests.cs | 16 +++ tests/NetVips.Tests/IoFuncsTests.cs | 30 +++++ 8 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/NetVips/net6.0/Image.cs create mode 100644 src/NetVips/net6.0/Source.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b15c99d..fe1fbb55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [3.0.0] - TBD ### Added - Add support for a single shared libvips binary on Windows ([#211](https://github.com/kleisauke/net-vips/issues/211)). +- Add `ReadOnlySpan` / `ReadOnlyMemory` overloads ([#225](https://github.com/kleisauke/net-vips/issues/225)). ### Removed - Drop support for .NET Standard 2.0 and Mono. NetVips now targets .NET 6 (`net6.0`) and .NET Framework 4.5.2 (`net452`) moving forward. diff --git a/src/NetVips/Internal/Vips.cs b/src/NetVips/Internal/Vips.cs index d85d0919..d2906b2b 100644 --- a/src/NetVips/Internal/Vips.cs +++ b/src/NetVips/Internal/Vips.cs @@ -289,6 +289,10 @@ internal static class VipsBlob [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_blob_get")] internal static extern nint Get(VipsBlobManaged blob, out nuint length); + + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_blob_copy")] + internal static extern unsafe nint Copy(void* data, nuint length); } internal static class VipsArea @@ -416,12 +420,24 @@ internal static class VipsImage internal static extern nint NewFromMemory(nint data, nuint 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 nint NewFromMemory(void* data, nuint 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 nint NewFromMemoryCopy(nint data, nuint 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 nint NewFromMemoryCopy(void* data, nuint size, int width, int height, + int bands, BandFormat format); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_image_new_matrix_from_array")] @@ -567,6 +583,11 @@ internal static class VipsForeign EntryPoint = "vips_foreign_find_load_buffer")] internal static extern nint FindLoadBuffer(nint data, ulong size); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_foreign_find_load_buffer")] + internal static extern unsafe nint FindLoadBuffer(void* data, ulong size); + [SuppressUnmanagedCodeSecurity] [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, EntryPoint = "vips_foreign_find_load_source")] @@ -617,6 +638,11 @@ internal static class VipsSource EntryPoint = "vips_source_new_from_file")] internal static extern nint NewFromFile(byte[] filename); + [SuppressUnmanagedCodeSecurity] + [DllImport(Libraries.Vips, CallingConvention = CallingConvention.Cdecl, + EntryPoint = "vips_source_new_from_blob")] + internal static extern nint 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 f5aaf643..ac6d5388 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 { /// /// Secret ref for . diff --git a/src/NetVips/net6.0/Image.cs b/src/NetVips/net6.0/Image.cs new file mode 100644 index 00000000..6cfded9b --- /dev/null +++ b/src/NetVips/net6.0/Image.cs @@ -0,0 +1,171 @@ +#if NET6_0_OR_GREATER + +using System; +using System.Runtime.InteropServices; +using NetVips.Internal; + +namespace NetVips; + +/// +/// 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 . + private 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, (nuint)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, (nuint)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, (nuint)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..2beb512c --- /dev/null +++ b/src/NetVips/net6.0/Source.cs @@ -0,0 +1,47 @@ +#if NET6_0_OR_GREATER + +using System; + +namespace NetVips; + +/// +/// 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, (nuint)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 800329a7..f7066772 100644 --- a/tests/NetVips.Tests/ConnectionTests.cs +++ b/tests/NetVips.Tests/ConnectionTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using Xunit; using Xunit.Abstractions; @@ -49,6 +50,21 @@ 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 = ""u8; + 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/IoFuncsTests.cs b/tests/NetVips.Tests/IoFuncsTests.cs index 1f49c65b..b0370a6b 100644 --- a/tests/NetVips.Tests/IoFuncsTests.cs +++ b/tests/NetVips.Tests/IoFuncsTests.cs @@ -138,6 +138,36 @@ public void TestNewFromMemoryPtr() Cache.Max = prevMax; } + [Fact] + public void TestNewFromMemoryReadOnly() + { + ReadOnlyMemory 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); + 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() {