19 Commits

Author SHA1 Message Date
6ffcd38cbc Add GCHandle to StbiStreamWrapper in TryLoad routines.
All checks were successful
Build / build (push) Successful in 1m56s
2024-11-18 20:17:31 +03:00
09b5238b24 Bump version and release notes.
All checks were successful
Build / build (push) Successful in 2m5s
2024-11-18 19:59:29 +03:00
7157db6bd0 Additional documentation. 2024-11-18 19:57:50 +03:00
df61cdc5aa Fix bug in UnpremultiplyOnLoad. 2024-11-18 19:57:38 +03:00
50c37e58fc Add calling convention attributes to unmanaged function pointers. 2024-11-18 19:33:28 +03:00
8e8c86b2c1 Remove unnecessary attribute. 2024-11-18 19:32:23 +03:00
ff83cb20f9 Enabled optimzations.
All checks were successful
Build / build (push) Successful in 2m2s
2024-07-23 23:21:12 +03:00
themixedupstuff
3801bc29f8 Update ReFuel.StbImage.csproj
All checks were successful
Build / build (push) Successful in 1m36s
2024-06-29 13:34:59 +02:00
bcd82900c7 Add STBIWDEF as RFEXTERN to build C file.
All checks were successful
Build / build (push) Successful in 1m35s
2024-06-19 12:19:33 +03:00
e759efacf6 [2.0.0-rc.2] Fix global options to be static.
All checks were successful
Build / build (push) Successful in 1m37s
2024-06-19 12:05:54 +03:00
d9a9e3962f Fix build error on older c# versions.
All checks were successful
Build / build (push) Successful in 1m35s
2024-06-19 11:50:36 +03:00
f976af3833 Increment version counter for 2.0.0-rc
Some checks failed
Build / build (push) Failing after 1m34s
2024-06-19 11:42:29 +03:00
9b21bb837b Move native calls to Stb.Native 2024-06-19 11:31:51 +03:00
e7d7f5f826 Implement stbi_write here. 2024-06-19 11:15:51 +03:00
e0e57b53f4 Increment version counter.
All checks were successful
Build / build (push) Successful in 1m36s
2024-06-16 21:36:11 +03:00
f135a65c96 Add stb_image_write.h support. 2024-06-16 21:34:59 +03:00
846e09054d Pull changes in build_native.sh
All checks were successful
Build / build (push) Successful in 2m0s
2024-06-14 22:11:33 +03:00
722d38be5c Update docker cmakes.
All checks were successful
Build / build (push) Successful in 1m23s
2024-06-14 22:00:20 +03:00
31f5e3938c Increment version.
All checks were successful
Build / build (push) Successful in 1m24s
2024-06-14 21:53:08 +03:00
13 changed files with 538 additions and 55 deletions

View File

@@ -5,9 +5,10 @@ project(rf_stbi LANGUAGES C VERSION 1.0)
if (NOT ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin"))
add_compile_options(-static-libgcc -static-libstdc++ -fno-exceptions)
endif()
add_compile_options(-O3)
add_library(stbi SHARED "rf_stbi.c")
install(
install(
TARGETS stbi
RUNTIME DESTINATION .
LIBRARY DESTINATION .)

View File

@@ -1,12 +0,0 @@
using System;
namespace Quik.Stb
{
[AttributeUsage(System.AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal sealed class NativeTypeNameAttribute : System.Attribute
{
public NativeTypeNameAttribute(string typename)
{
}
}
}

View File

@@ -1,19 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>7.3</LangVersion>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<RuntimeIdentifiers>linux-arm;linux-arm64;linux-x64;win-x86;win-x64;osx-arm64;osx-x64</RuntimeIdentifiers>
<RootNamespace>ReFuel.Stb</RootNamespace>
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup>
<!-- Nuget Properties. -->
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ReFuel.StbImage</PackageId>
<Version>1.0.0</Version>
<Version>2.0.2-rc.1</Version>
<Authors>STBI Authors, H. Utku Maden</Authors>
<Description>
A C# wrapper for the ubiquitous stb_image.h and stb_image_write.h library.
@@ -21,6 +22,21 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<PackageIcon>images\icon.png</PackageIcon>
<PackageProjectUrl>https://refuel.mixedup.dev/docs/ReFuel.StbImage.html</PackageProjectUrl>
<RepositoryUrl>https://git.mixedup.dev/ReFuel/ReFuel.StbImage</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>stb; stb_image; stbi; image; load; save; read; write</PackageTags>
<PackageReleaseNotes># 2.0.2
* Fixed calling convention of unmanaged function pointers. (Thanks NogginBops!)
* Allocating a GC handle to StbiStreamWrapper class and passing it as userdata into stbi in order to prevent
the object from being prematurely collected by the garbage collector when optimizations are enabled in Release mode.
# 2.0.1
* Enabled optimizations across the board for native and managed assemblies.
# 2.0.0
* Exposed stbi_image_write.h
* Moved native function calls to ReFuel.Stb.Native</PackageReleaseNotes>
</PropertyGroup>
<!--
@@ -30,8 +46,8 @@
that determines a lot of load paths for projects.
-->
<ItemGroup>
<Content Include="*.md" Pack="true" PackagePath="/"/>
<None Include="rf_stbimage.png" Pack="true" PackagePath="images\icon.png"/>
<Content Include="*.md" Pack="true" PackagePath="/" />
<None Include="rf_stbimage.png" Pack="true" PackagePath="images\icon.png" />
<Content Include="runtimes/linux-arm/native/*.so">
<PackagePath>runtimes/linux-arm/native/</PackagePath>
<Pack>true</Pack>

View File

@@ -1,4 +1,6 @@
using ReFuel.Stb.Native;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
@@ -31,6 +33,7 @@ namespace ReFuel.Stb
/// Internal image format.
/// </summary>
public StbiImageFormat Format { get; }
/// <summary>
/// True if the image is a floating point image.
/// </summary>
@@ -50,6 +53,72 @@ namespace ReFuel.Stb
Dispose(false);
}
/// <summary>
/// Get a safe span to the image pointer.
/// </summary>
/// <typeparam name="T">The pixel type.</typeparam>
/// <returns>A span to the image data.</returns>
/// <exception cref="Exception">The image uses an unexpected image format.</exception>
public ReadOnlySpan<T> AsSpan<T>() where T : unmanaged
{
int sz = Format switch
{
StbiImageFormat.Grey => 1,
StbiImageFormat.GreyAlpha => 2,
StbiImageFormat.Rgb => 3,
StbiImageFormat.Rgba => 4,
_ => throw new Exception("unknown image format")
} * (IsFloat ? sizeof(float) : sizeof(byte));
return new ReadOnlySpan<T>((T*)ImagePointer, Width * Height * sz / sizeof(T));
}
/// <summary>
/// Write image to a PNG file.
/// </summary>
/// <param name="dest">The destination stream.</param>
/// <remarks>
/// Incurs a conversion cost if the image format is not a byte format. Affected by non-thread safe global options.
/// </remarks>
public void WritePng(Stream dest) => WritePng(AsSpan<byte>(), Width, Height, Format, dest, isFloat: IsFloat);
/// <summary>
/// Write image to a BMP file.
/// </summary>
/// <param name="dest">The destination stream.</param>
/// <remarks>
/// Incurs a conversion cost if the image format is not a byte format. Affected by non-thread safe global options.
/// </remarks>
public void WriteBmp(Stream dest) => WriteBmp(AsSpan<byte>(), Width, Height, Format, dest, isFloat: IsFloat);
/// <summary>
/// Write image to a TGA file.
/// </summary>
/// <param name="dest">The destination stream.</param>
/// <remarks>
/// Incurs a conversion cost if the image format is not a byte format. Ignores alpha channel. Affected by non-thread safe global options.
/// </remarks>
public void WriteTga(Stream dest) => WriteTga(AsSpan<byte>(), Width, Height, Format, dest, isFloat: IsFloat);
/// <summary>
/// Write image to a PNG file.
/// </summary>
/// <param name="dest">The destination stream.</param>
/// <remarks>
/// Incurs a conversion cost if the image format is not a float format. Affected by non-thread safe global options.
/// </remarks>
public void WriteHdr(Stream dest) => WriteHdr(AsSpan<byte>(), Width, Height, Format, dest, isFloat: IsFloat);
/// <summary>
/// Write image to a PNG file.
/// </summary>
/// <param name="dest">The destination stream.</param>
/// <param name="quality">The JPEG quality factor.</param>
/// <remarks>
/// Incurs a conversion cost if the image format is not a byte format. Ignores alpha channel. Affected by non-thread safe global options.
/// </remarks>
public void WriteJpg(Stream dest, int quality = 90) => WriteJpg(AsSpan<byte>(), Width, Height, Format, dest, quality: quality, isFloat: IsFloat);
public void Dispose() => Dispose(true);
private void Dispose(bool disposing)
@@ -68,7 +137,12 @@ namespace ReFuel.Stb
/// <summary>
/// Set to flip the y-axis of loaded images on load.
/// </summary>
public static bool FlipVerticallyOnLoad { set => Stbi.set_flip_vertically_on_load(1); }
public static bool FlipVerticallyOnLoad { set => Stbi.set_flip_vertically_on_load(value ? 1 : 0); }
/// <summary>
/// Set to flip the y-axis of saved images.
/// </summary>
public static bool FlipVerticallyOnSave { set => Stbi.flip_vertically_on_write(value ? 1 : 0); }
/// <summary>
/// Set to unpremultiply images on load.
@@ -77,7 +151,51 @@ namespace ReFuel.Stb
/// According to the stb_image documentation, only iPhone PNG images
/// can come with premultiplied alpha.
/// </remarks>
public static bool UnpremultiplyOnLoad { set => Stbi.set_unpremultiply_on_load(1); }
public static bool UnpremultiplyOnLoad { set => Stbi.set_unpremultiply_on_load(value ? 1 : 0); }
/// <summary>
/// Force a filter on PNG filter when saving.
/// </summary>
/// <remarks>
/// -1 for auto, 0 through 5 to pick a filter. Higher is more. Not thread safe.
/// </remarks>
public static int WriteForcePngFilter
{
get => Stbi.write_force_png_filter;
set
{
if (value < -1 || value > 5)
{
throw new ArgumentOutOfRangeException(nameof(value), "The PNG filter must be in the range 0 to 5, or -1 for auto.");
}
Stbi.write_force_png_filter = value;
}
}
/// <summary>
/// Change the PNG compression level on save.
/// </summary>
/// <remarks>
/// Higher is more. Defaults to 8. Not thread safe.
/// </remarks>
public static int WritePngCompressionLevel
{
get => Stbi.write_png_compression_level;
set => Stbi.write_png_compression_level = value;
}
/// <summary>
/// Enable run length encoding on TGA images on save.
/// </summary>
/// <remarks>
/// Not thread safe.
/// </remarks>
public static bool WriteTgaEnableRLE
{
get => Stbi.write_tga_with_rle != 0;
set => Stbi.write_tga_with_rle = value ? 1 : 0;
}
/// <summary>
/// Try loading an image, without raising exceptions.
@@ -85,27 +203,29 @@ namespace ReFuel.Stb
/// <param name="image">The resulting image.</param>
/// <param name="stream">Source stream.</param>
/// <param name="format">The desired image format.</param>
/// <param name="asFloat">The buffer uses floating point pixels if true.</param>
/// <returns>True on success.</returns>
public static bool TryLoad(out StbImage image, Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
public static bool TryLoad([NotNullWhen(true)] out StbImage? image, Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
GCHandle gch = GCHandle.Alloc(wrapper, GCHandleType.Normal);
wrapper.CreateCallbacks(out stbi_io_callbacks cb);
stream.Position = 0;
IntPtr imagePtr;
if (isFloat)
if (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_callbacks(&cb, null, &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.loadf_from_callbacks(&cb, (void*)(IntPtr)gch, &x, &y, &iFormat, (int)format);
}
else
{
imagePtr = (IntPtr)Stbi.load_from_callbacks(&cb, null, &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.load_from_callbacks(&cb, (void*)(IntPtr)gch, &x, &y, &iFormat, (int)format);
}
gch.Free();
if (imagePtr != IntPtr.Zero)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, asFloat);
return true;
}
else
@@ -121,26 +241,26 @@ namespace ReFuel.Stb
/// <param name="image">The resulting image.</param>
/// <param name="span">Source memory span.</param>
/// <param name="format">The desired image format.</param>
/// <param name="asFloat">The buffer uses floating point pixels if true.</param>
/// <returns>True on success.</returns>
public static bool TryLoad<T>(out StbImage image, ReadOnlySpan<T> span, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
where T : unmanaged
public static bool TryLoad([NotNullWhen(true)] out StbImage? image, ReadOnlySpan<byte> span, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
IntPtr imagePtr = IntPtr.Zero;
int x, y, iFormat;
fixed (byte *ptr = MemoryMarshal.AsBytes(span))
{
if (isFloat)
if (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length, &x, &y, &iFormat, (int)format);
}
else
{
imagePtr = (IntPtr)Stbi.load_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.load_from_memory(ptr, span.Length, &x, &y, &iFormat, (int)format);
}
if (imagePtr != IntPtr.Zero)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, asFloat);
return true;
}
else
@@ -156,15 +276,16 @@ namespace ReFuel.Stb
/// </summary>
/// <param name="stream">The stream to load from.</param>
/// <param name="format">The desired image format.</param>
/// <param name="asFloat">The buffer uses floating point pixels if true.</param>
/// <returns>The image object.</returns>
public static StbImage Load(Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
public static StbImage Load(Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
if (TryLoad(out StbImage image, stream, format, isFloat))
if (TryLoad(out StbImage? image, stream, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
@@ -173,16 +294,16 @@ namespace ReFuel.Stb
/// </summary>
/// <param name="span">The span of memory to load from.</param>
/// <param name="format">The desired image format.</param>
/// <param name="asFloat">The buffer uses floating point pixels if true.</param>
/// <returns>The image object.</returns>
public static StbImage Load<T>(ReadOnlySpan<T> span, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
where T : unmanaged
public static StbImage Load(ReadOnlySpan<byte> span, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
if (TryLoad(out StbImage image, span, format, isFloat))
if (TryLoad(out StbImage? image, span, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
@@ -230,5 +351,223 @@ namespace ReFuel.Stb
return result != 0;
}
}
private static int Components(StbiImageFormat format) => format switch
{
StbiImageFormat.Grey => 1,
StbiImageFormat.GreyAlpha => 2,
StbiImageFormat.Rgb => 3,
StbiImageFormat.Rgba => 4,
_ => throw new ArgumentException("Expected a fully qualified format.")
};
private static byte[] ConvertFloatToByte<T>(ReadOnlySpan<T> source, int width, int height, int components)
where T : unmanaged
{
byte[] conversion = new byte[width * height * components];
ReadOnlySpan<float> dataAsFloat = MemoryMarshal.Cast<T, float>(source);
for (int i = 0; i<conversion.Length; i++)
{
conversion[i] = (byte) Math.Clamp(MathF.Round(dataAsFloat[i]* 255), 0, 255);
}
return conversion;
}
private static float[] ConvertByteToFloat<T>(ReadOnlySpan<T> source, int width, int height, int components)
where T : unmanaged
{
float[] conversion = new float[width * height * components];
ReadOnlySpan<byte> dataAsByte = MemoryMarshal.Cast<T, byte>(source);
for (int i = 0; i < conversion.Length; i++)
{
conversion[i] = Math.Clamp(dataAsByte[i]/255f, 0f, 1f);
}
return conversion;
}
/// <summary>
/// Write any image to a PNG file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat"/>.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="isFloat">True if the pixel format in data is a floating point format.</param>
/// <remarks>
/// This will incur a conversion cost if the pixel format is not a byte format. Affected by global non-thread safe options.
/// </remarks>
public static void WritePng<T>(ReadOnlySpan<T> data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan<byte> source;
byte[]? conversion;
if (isFloat)
{
conversion = ConvertFloatToByte(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.AsBytes(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (byte *ptr = source)
Stbi.write_png_to_func(wrapper, null, width, height, components, ptr, width * components);
}
/// <summary>
/// Write any image to a BMP file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat"/>.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="isFloat">True if the pixel format in data is a floating point format.</param>
/// <remarks>
/// This will incur a conversion cost if the pixel format is not a byte format. Ignores the alpha channel. Affected by global non-thread safe options.
/// </remarks>
public static void WriteBmp<T>(ReadOnlySpan<T> data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan<byte> source;
byte[]? conversion;
if (isFloat)
{
conversion = ConvertFloatToByte(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.AsBytes(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (byte* ptr = source)
Stbi.write_bmp_to_func(wrapper, null, width, height, components, ptr);
}
/// <summary>
/// Write any image to a TGA file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat"/>.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="isFloat">True if the pixel format in data is a floating point format.</param>
/// <remarks>
/// This will incur a conversion cost if the pixel format is not a byte format. Affected by global non-thread safe options.
/// </remarks>
public static void WriteTga<T>(ReadOnlySpan<T> data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan<byte> source;
byte[]? conversion;
if (isFloat)
{
conversion = ConvertFloatToByte(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.AsBytes(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (byte* ptr = source)
Stbi.write_tga_to_func(wrapper, null, width, height, components, ptr);
}
/// <summary>
/// Write any image to a PNG file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat"/>.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="isFloat">True if the pixel format in data is a floating point format.</param>
/// <remarks>
/// This will incur a conversion cost if the pixel format is not a float format. Affected by global non-thread safe options.
/// </remarks>
public static void WriteHdr<T>(ReadOnlySpan<T> data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan<float> source;
float[]? conversion;
if (!isFloat)
{
conversion = ConvertByteToFloat(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.Cast<T, float>(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (float* ptr = source)
Stbi.write_hdr_to_func(wrapper, null, width, height, components, ptr);
}
/// <summary>
/// Write any image to a PNG file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat"/>.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="quality">The JPEG quality factor.</param>
/// <param name="isFloat">True if the pixel format in data is a floating point format.</param>
/// <remarks>
/// This will incur a conversion cost if the pixel format is not a byte format. Ignores the alpha channel. Affected by global non-thread safe options.
/// </remarks>
public static void WriteJpg<T>(ReadOnlySpan<T> data, int width, int height, StbiImageFormat format, Stream destination, int quality = 90, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan<byte> source;
byte[]? conversion;
if (isFloat)
{
conversion = ConvertFloatToByte(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.AsBytes(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (byte* ptr = source)
Stbi.write_jpg_to_func(wrapper, null, width, height, components, ptr, quality);
}
}
}
}

View File

@@ -1,14 +1,16 @@
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Reflection;
namespace ReFuel.Stb
namespace ReFuel.Stb.Native
{
/// <summary>
/// Direct access to the native STBI function calls.
/// </summary>
public unsafe static partial class Stbi
{
private delegate void FailedAssertProc(byte *expression, byte *file, int line, byte *function);
private static IntPtr stbiHandle;
private static readonly string[] LibraryNames = new string[]
{
@@ -29,22 +31,33 @@ namespace ReFuel.Stb
static Stbi()
{
NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), Resolver);
// Dummy call to fail_reason so we have a handle to STBI.
failure_reason();
// Load global address pointers.
_tga_with_rle_ptr = (int*)NativeLibrary.GetExport(stbiHandle, "stbi_write_tga_with_rle");
_png_compression_level_ptr = (int*)NativeLibrary.GetExport(stbiHandle, "stbi_write_png_compression_level");
_force_png_filter_ptr = (int*)NativeLibrary.GetExport(stbiHandle, "stbi_write_force_png_filter");
}
private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName != "stbi")
return IntPtr.Zero;
else if (stbiHandle != IntPtr.Zero)
return stbiHandle;
foreach (string name in LibraryNames)
{
if (NativeLibrary.TryLoad(name, assembly, searchPath, out IntPtr handle))
if (NativeLibrary.TryLoad(name, assembly, searchPath, out stbiHandle))
{
return handle;
return stbiHandle;
}
}
return NativeLibrary.Load(libraryName);
return stbiHandle = NativeLibrary.Load(libraryName);
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
namespace ReFuel.Stb.Native
{
public enum StbiEnum : uint
{

View File

@@ -1,5 +1,10 @@
using ReFuel.Stb.Native;
namespace ReFuel.Stb
{
/// <summary>
/// Enumeration of supported STBI image formats.
/// </summary>
public enum StbiImageFormat
{
Default = (int)StbiEnum.STBI_default,

View File

@@ -1,23 +1,52 @@
using ReFuel.Stb.Native;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
{
/// <summary>
/// Pointer to STBI stream read function.
/// </summary>
/// <param name="userdata">User provided userdata pointer.</param>
/// <param name="buffer">C array to read into.</param>
/// <param name="count">Size of the C array in bytes.</param>
/// <returns>The number of bytes read from the stream.</returns>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int StbiReadProc(void *userdata, byte* buffer, int count);
/// <summary>
/// Pointer to STBI stream skip function.
/// </summary>
/// <param name="userdata">User provided userdata pointer.</param>
/// <param name="count">Number of bytes to skip.</param>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void StbiSkipProc(void *userdata, int count);
/// <summary>
/// Pointer to STBI stream end of file function.
/// </summary>
/// <param name="userdata">User provided userdata pointer.</param>
/// <returns>Non-zero value if the end of the stream has been reached.</returns>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int StbiEofProc(void *userdata);
/// <summary>
/// An easy to use stream wrapper for use with STBI image load functions.
/// </summary>
public unsafe class StbiStreamWrapper : IDisposable
{
private Stream _stream;
private bool _keepOpen;
private readonly stbi_io_callbacks _callbacks;
private readonly Stream _stream;
private readonly bool _keepOpen;
private bool _isDisposed;
private StbiReadProc _readCb;
private StbiSkipProc _skipCb;
private StbiEofProc _eofCb;
public ref readonly stbi_io_callbacks Callbacks => ref _callbacks;
public StbiStreamWrapper(Stream stream, bool keepOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
@@ -28,6 +57,11 @@ namespace ReFuel.Stb
_readCb = ReadCb;
_skipCb = SkipCb;
_eofCb = EofCb;
_callbacks = default;
_callbacks.read = Marshal.GetFunctionPointerForDelegate<StbiReadProc>(_readCb);
_callbacks.skip = Marshal.GetFunctionPointerForDelegate<StbiSkipProc>(_skipCb);
_callbacks.eof = Marshal.GetFunctionPointerForDelegate<StbiEofProc>(_eofCb);
}
public void CreateCallbacks(out stbi_io_callbacks cb)
@@ -65,4 +99,33 @@ namespace ReFuel.Stb
_isDisposed = true;
}
}
}
/// <summary>
/// An easy to use stream wrapper for STBI image write functions.
/// </summary>
/// <remarks>Keep object alive for the duration of the write operation.</remarks>
public class StbiWriteStreamWrapper
{
private readonly Stream _stream;
private readonly StbiWriteProc _cb;
public IntPtr Callback { get; }
public StbiWriteStreamWrapper(Stream stream)
{
_stream = stream;
unsafe
{
_cb = WriteCb;
}
Callback = Marshal.GetFunctionPointerForDelegate(_cb);
}
private unsafe void WriteCb(void *context, void *data, int size)
{
_stream.Write(new ReadOnlySpan<byte>((byte*)data, size));
}
public static implicit operator IntPtr(in StbiWriteStreamWrapper wrapper) => wrapper.Callback;
}
}

57
StbiWrite.cs Normal file
View File

@@ -0,0 +1,57 @@
using System;
using System.Runtime.InteropServices;
namespace ReFuel.Stb.Native
{
/// <summary>
/// Procedure to STBI image write function.
/// </summary>
/// <param name="context">User provided context pointer.</param>
/// <param name="data">C Array of data to write.</param>
/// <param name="size">Size of the C array in bytes.</param>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void StbiWriteProc(void* context, void* data, int size);
public unsafe partial class Stbi
{
private static readonly int* _tga_with_rle_ptr;
private static readonly int* _png_compression_level_ptr;
private static readonly int* _force_png_filter_ptr;
public static int write_tga_with_rle
{
get => *_tga_with_rle_ptr;
set => *_tga_with_rle_ptr = value;
}
public static int write_png_compression_level
{
get => *_png_compression_level_ptr;
set => *_png_compression_level_ptr = value;
}
public static int write_force_png_filter
{
get => *_force_png_filter_ptr;
set => *_force_png_filter_ptr = value;
}
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_png_to_func")]
public static extern int write_png_to_func(IntPtr func, void* context, int w, int h, int comp, void* data, int stride_in_bytes);
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_bmp_to_func")]
public static extern int write_bmp_to_func(IntPtr func, void* context, int w, int h, int comp, void* data);
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_tga_to_func")]
public static extern int write_tga_to_func(IntPtr func, void* context, int w, int h, int comp, void* data);
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_hdr_to_func")]
public static extern int write_hdr_to_func(IntPtr func, void* context, int w, int h, int comp, void* data);
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_jpg_to_func")]
public static extern int write_jpg_to_func(IntPtr func, void* context, int w, int h, int comp, void* data, int quality);
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_flip_vertically_on_write")]
public static extern int flip_vertically_on_write(int value);
}
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash
cd $(dirname "$0")
./docker-cross-compiler/sh/build_native.sh .
dotnet build
dotnet build -c Release

View File

@@ -7,6 +7,7 @@
#define STBI_NO_THREAD_LOCALS 1
#include "stb/stb_image.h"
#define STBIWDEF RFEXTERN
#define STBI_WRITE_NO_STDIO 1
#include "stb/stb_image_write.h"

2
stb

Submodule stb updated: 013ac3bedd...f7f20f39fe