Skip to content

Commit

Permalink
Add ReadOnlySpan<T> / ReadOnlyMemory<T> overloads
Browse files Browse the repository at this point in the history
Implements:
- `Source.NewFromMemory`, `Image.FindLoadBuffer` and
  `Image.NewFromBuffer` for `ReadOnlySpan<byte>`.
- `Image.NewFromMemory` for `ReadOnlyMemory<T>`.
- `Image.NewFromMemoryCopy` for `ReadOnlySpan<T>`.
  • Loading branch information
kleisauke committed Jan 11, 2024
1 parent 49c4a15 commit d5243b5
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 4 deletions.
26 changes: 26 additions & 0 deletions src/NetVips/Internal/Vips.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions src/NetVips/NetVips.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<StartupObject />
<Platforms>x64;x86;ARM64;ARM32</Platforms>
<Optimize>true</Optimize>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
Expand Down
3 changes: 1 addition & 2 deletions src/NetVips/Source.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace NetVips
/// <summary>
/// An input connection.
/// </summary>
public class Source : Connection
public partial class Source : Connection
{
// private static Logger logger = LogManager.GetCurrentClassLogger();

Expand Down Expand Up @@ -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)
Expand Down
174 changes: 174 additions & 0 deletions src/NetVips/net6.0/Image.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#if NET6_0_OR_GREATER

namespace NetVips
{
using System;
using System.Runtime.InteropServices;
using Internal;

/// <summary>
/// Wrap a <see cref="VipsImage"/> object.
/// </summary>
public partial class Image
{
#region helpers

/// <summary>
/// Find the name of the load operation vips will use to load a buffer.
/// </summary>
/// <remarks>
/// For example "VipsForeignLoadJpegBuffer". You can use this to work out what
/// options to pass to <see cref="NewFromBuffer(ReadOnlySpan{byte}, string, Enums.Access?, Enums.FailOn?, VOption)"/>.
/// </remarks>
/// <param name="data">The buffer to test.</param>
/// <param name="size">Length of the buffer.</param>
/// <returns>The name of the load operation, or <see langword="null"/>.</returns>
internal static unsafe string FindLoadBuffer(void* data, ulong size) =>
Marshal.PtrToStringAnsi(VipsForeign.FindLoadBuffer(data, size));

/// <summary>
/// Find the name of the load operation vips will use to load a buffer.
/// </summary>
/// <remarks>
/// For example "VipsForeignLoadJpegBuffer". You can use this to work out what
/// options to pass to <see cref="NewFromBuffer(ReadOnlySpan{byte}, string, Enums.Access?, Enums.FailOn?, VOption)"/>.
/// </remarks>
/// <param name="data">The buffer to test.</param>
/// <returns>The name of the load operation, or <see langword="null"/>.</returns>
public static unsafe string FindLoadBuffer(ReadOnlySpan<byte> data)
{
fixed (byte* dataFixed = data)
{
return FindLoadBuffer(dataFixed, (ulong)data.Length);
}
}

#endregion

#region constructors

/// <summary>
/// Load a formatted image from memory.
/// </summary>
/// <remarks>
/// This behaves exactly as <see cref="NewFromFile"/>, but the image is
/// loaded from the memory object rather than from a file. The memory
/// object can be a string or buffer.
/// </remarks>
/// <param name="data">The memory object to load the image from.</param>
/// <param name="strOptions">Load options as a string. Use <see cref="string.Empty"/> for no options.</param>
/// <param name="access">Hint the expected access pattern for the image.</param>
/// <param name="failOn">The type of error that will cause load to fail. By
/// default, loaders are permissive, that is, <see cref="Enums.FailOn.None"/>.</param>
/// <param name="kwargs">Optional options that depend on the load operation.</param>
/// <returns>A new <see cref="Image"/>.</returns>
/// <exception cref="VipsException">If unable to load from <paramref name="data"/>.</exception>
public static unsafe Image NewFromBuffer(
ReadOnlySpan<byte> 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;
}
}

/// <summary>
/// Wrap an image around a memory array.
/// </summary>
/// <param name="data">A <see cref="ReadOnlyMemory{T}"/>.</param>
/// <param name="width">Image width in pixels.</param>
/// <param name="height">Image height in pixels.</param>
/// <param name="bands">Number of bands.</param>
/// <param name="format">Band format.</param>
/// <returns>A new <see cref="Image"/>.</returns>
/// <exception cref="VipsException">If unable to make image from <paramref name="data"/>.</exception>
public static unsafe Image NewFromMemory<T>(
ReadOnlyMemory<T> 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;
}

/// <summary>
/// Like <see cref="NewFromMemory{T}(ReadOnlyMemory{T}, int, int, int, Enums.BandFormat)"/>, but
/// for <see cref="ReadOnlySpan{T}"/>, so we must copy as it could be allocated on the stack.
/// </summary>
/// <param name="data">A <see cref="ReadOnlySpan{T}"/>.</param>
/// <param name="width">Image width in pixels.</param>
/// <param name="height">Image height in pixels.</param>
/// <param name="bands">Number of bands.</param>
/// <param name="format">Band format.</param>
/// <returns>A new <see cref="Image"/>.</returns>
/// <exception cref="VipsException">If unable to make image from <paramref name="data"/>.</exception>
public static unsafe Image NewFromMemoryCopy<T>(
ReadOnlySpan<T> 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
48 changes: 48 additions & 0 deletions src/NetVips/net6.0/Source.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if NET6_0_OR_GREATER

namespace NetVips
{
using System;

/// <summary>
/// An input connection.
/// </summary>
public partial class Source
{
/// <summary>
/// Make a new source from a memory object.
/// </summary>
/// <remarks>
/// Make a new source that is attached to the memory object. For example:
/// <code language="lang-csharp">
/// using var source = Source.NewFromMemory(data);
/// </code>
/// You can pass this source to (for example) <see cref="Image.NewFromSource"/>.
/// </remarks>
/// <param name="data">The memory object.</param>
/// <returns>A new <see cref="Source"/>.</returns>
/// <exception cref="VipsException">If unable to create a new <see cref="Source"/> from <paramref name="data"/>.</exception>
public static unsafe Source NewFromMemory(ReadOnlySpan<byte> 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
18 changes: 18 additions & 0 deletions tests/NetVips.Tests/ConnectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace NetVips.Tests
{
using System;
using System.IO;
using System.Text;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -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<byte> input = Encoding.UTF8
.GetBytes("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" />")
.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()
{
Expand Down
2 changes: 1 addition & 1 deletion tests/NetVips.Tests/ForeignTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
32 changes: 31 additions & 1 deletion tests/NetVips.Tests/IoFuncsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -143,6 +143,36 @@ public void TestNewFromMemoryPtr()
Cache.Max = prevMax;
}

[Fact]
public void TestNewFromMemoryReadOnly()
{
var s = new ReadOnlyMemory<byte>(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<byte> 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()
{
Expand Down

0 comments on commit d5243b5

Please sign in to comment.