diff --git a/.gitignore b/.gitignore
index 8a5925c..b132b16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,5 @@ riderModule.iml
.idea
.vscode
nuget_repo
-*.redist/out
-*.redist/runtimes
\ No newline at end of file
+**/out
+**/runtimes
\ No newline at end of file
diff --git a/Quik.StbImage.redist/Quik.StbImage.redist.nuspec b/Quik.StbImage.redist/Quik.StbImage.redist.nuspec
deleted file mode 100644
index 8e6f8b7..0000000
--- a/Quik.StbImage.redist/Quik.StbImage.redist.nuspec
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- Quik.StbImage.redist
- 1.0.0
- STBI Authors, H. Utku Maden
- utkumaden
-
- This is a redistribution of STBI for use with the QUIK project.
-
- Compiled with `STBI_NO_STDIO` and a custom assert function. Call
- `quik_stbi_failed_assert_store` to replace.
-
- native
-
-
-
-
-
\ No newline at end of file
diff --git a/Quik.StbImage.redist/CMakeLists.txt b/Quik.StbImage/CMakeLists.txt
similarity index 100%
rename from Quik.StbImage.redist/CMakeLists.txt
rename to Quik.StbImage/CMakeLists.txt
diff --git a/Quik.StbImage/Quik.StbImage.csproj b/Quik.StbImage/Quik.StbImage.csproj
index 6887c90..60fc67e 100644
--- a/Quik.StbImage/Quik.StbImage.csproj
+++ b/Quik.StbImage/Quik.StbImage.csproj
@@ -5,10 +5,26 @@
disable
7.3
True
+ linux-arm;linux-arm64;linux-x64;win-x86;win-x64
+
+
+
+
+ True
+ Quik.StbImage
+ 1.0.0
+ STBI Authors, H. Utku Maden
+
+ A C# wrapper for the ubiquitous Stb Image library.
+
-
+
+ runtimes
+ true
+ PreserveNewest
+
diff --git a/Quik.StbImage/Stbi.Manual.cs b/Quik.StbImage/Stbi.Manual.cs
new file mode 100644
index 0000000..53e4f96
--- /dev/null
+++ b/Quik.StbImage/Stbi.Manual.cs
@@ -0,0 +1,67 @@
+using System;
+using System.IO;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Reflection;
+
+namespace Quik.Stb
+{
+ public unsafe static partial class Stbi
+ {
+ private delegate void FailedAssertProc(byte *expression, byte *file, int line, byte *function);
+
+ private static readonly string[] LibraryNames = new string[]
+ {
+ "runtimes/libstbi.dll",
+ "runtimes/libstbi.so",
+ "runtimes/libstbi.dylib",
+ "libstbi.dll",
+ "libstbi.so",
+ "libstbi.dylib",
+ };
+
+ static Stbi()
+ {
+ NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), Resolver);
+
+ quik_stbi_failed_assert_store(Marshal.GetFunctionPointerForDelegate(FailedAssert));
+ }
+
+ private const DllImportSearchPath SearchPath =
+ DllImportSearchPath.ApplicationDirectory |
+ DllImportSearchPath.UserDirectories |
+ DllImportSearchPath.AssemblyDirectory |
+ DllImportSearchPath.UseDllDirectoryForDependencies;
+
+ private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
+ {
+ Debugger.Break();
+ if (libraryName != "stbi")
+ return IntPtr.Zero;
+
+ foreach (string name in LibraryNames)
+ {
+ if (NativeLibrary.TryLoad(name, assembly, SearchPath, out IntPtr handle))
+ {
+ return handle;
+ }
+ }
+
+ return NativeLibrary.Load(libraryName);
+ }
+
+ private static void FailedAssert(byte *expression, byte *file, int line, byte *function)
+ {
+ string expr = expression == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)expression);
+ string f = file == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)file);
+ string func = function == null ? string.Empty : Marshal.PtrToStringUTF8((IntPtr)function);
+
+ Exception ex = new Exception("Assert failed in native stbi code.");
+ ex.Data.Add("Expression", expr);
+ ex.Data.Add("File", f);
+ ex.Data.Add("Line", line);
+ ex.Data.Add("Function", func);
+ throw ex;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quik.StbImage/StbiStreamWrapper.cs b/Quik.StbImage/StbiStreamWrapper.cs
new file mode 100644
index 0000000..4038c48
--- /dev/null
+++ b/Quik.StbImage/StbiStreamWrapper.cs
@@ -0,0 +1,60 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Quik.Stb
+{
+ public unsafe class StbiStreamWrapper : IDisposable
+ {
+ private Stream _stream;
+ private bool _keepOpen;
+ private bool _isDisposed;
+
+ private delegate int ReadProc(void *userdata, byte* buffer, int count);
+ private delegate void SkipProc(void *userdata, int count);
+ private delegate int Eof(void *userdata);
+
+ public StbiStreamWrapper(Stream stream, bool keepOpen = false)
+ {
+ if (stream == null) throw new ArgumentNullException(nameof(stream));
+
+ _stream = stream;
+ _keepOpen = keepOpen;
+ }
+
+ public void CreateCallbacks(out stbi_io_callbacks cb)
+ {
+ cb = default;
+ cb.read = Marshal.GetFunctionPointerForDelegate(ReadCb);
+ cb.skip = Marshal.GetFunctionPointerForDelegate(SkipCb);
+ cb.eof = Marshal.GetFunctionPointerForDelegate(EofCb);
+ }
+
+ private int ReadCb(void *userdata, byte* buffer, int count)
+ {
+ Span bytes = new Span(buffer, count);
+ return _stream.Read(bytes);
+ }
+
+ private void SkipCb(void *userdata, int count)
+ {
+ _stream.Seek(count, SeekOrigin.Current);
+ }
+
+ private int EofCb(void *userdata)
+ {
+ if (!_stream.CanRead || _stream.Position == _stream.Length)
+ return 1;
+ return 0;
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+
+ if (!_keepOpen) _stream.Dispose();
+
+ _isDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quik.StbImage.redist/quik_stbi.c b/Quik.StbImage/quik_stbi.c
similarity index 100%
rename from Quik.StbImage.redist/quik_stbi.c
rename to Quik.StbImage/quik_stbi.c
diff --git a/Quik.StbImage.redist/quik_stbi.h b/Quik.StbImage/quik_stbi.h
similarity index 100%
rename from Quik.StbImage.redist/quik_stbi.h
rename to Quik.StbImage/quik_stbi.h
diff --git a/Quik.StbTrueType.redist/Quik.StbTrueType.redist.nuspec b/Quik.StbTrueType.redist/Quik.StbTrueType.redist.nuspec
index 196fa55..7a360ba 100644
--- a/Quik.StbTrueType.redist/Quik.StbTrueType.redist.nuspec
+++ b/Quik.StbTrueType.redist/Quik.StbTrueType.redist.nuspec
@@ -14,6 +14,6 @@
native
-
+
\ No newline at end of file
diff --git a/Quik.sln b/Quik.sln
index 843c834..c854c95 100644
--- a/Quik.sln
+++ b/Quik.sln
@@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbImage", "Quik.StbIm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbTrueType", "Quik.StbTrueType\Quik.StbTrueType.csproj", "{AD5B02B0-F957-4816-8CE9-5F278B856C70}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5E87AF9C-AC12-4E48-99B1-CBEC0C97B624}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.StbImage.Tests", "tests\Quik.StbImage.Tests\Quik.StbImage.Tests.csproj", "{AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,5 +71,20 @@ Global
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x64.Build.0 = Release|Any CPU
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x86.ActiveCfg = Release|Any CPU
{AD5B02B0-F957-4816-8CE9-5F278B856C70}.Release|x86.Build.0 = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x64.Build.0 = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Debug|x86.Build.0 = Debug|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x64.ActiveCfg = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x64.Build.0 = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x86.ActiveCfg = Release|Any CPU
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {AFF181CF-D51E-4E16-B3C6-38ED1E1FF615} = {5E87AF9C-AC12-4E48-99B1-CBEC0C97B624}
EndGlobalSection
EndGlobal
diff --git a/sh/build_native.sh b/sh/build_native.sh
index ea8722d..3a87473 100755
--- a/sh/build_native.sh
+++ b/sh/build_native.sh
@@ -1,7 +1,5 @@
#!/bin/bash
-source $(dirname "$0")/quik_build_native.sh
ARCHS="linux-arm linux-arm64 linux-x64 linux-x86 win-x64 win-x86"
-quik_build_native "Quik.StbImage.redist" "$ARCHS"
-cd ..
-quik_build_native "Quik.StbTrueType.redist" "$ARCHS"
+sh/quik_build_native.sh "Quik.StbImage" "$ARCHS"
+./quik_build_native.sh "Quik.StbTrueType.redist" "$ARCHS"
diff --git a/sh/quik_build_native.sh b/sh/quik_build_native.sh
index ba22f43..e571ca8 100755
--- a/sh/quik_build_native.sh
+++ b/sh/quik_build_native.sh
@@ -3,36 +3,29 @@
# $1 Source path of the project.
# $2 Target architecture list.
-quik_build_native () {
- SRC=$1
- NAME=$(dirname $SRC)
- ARCHS=$2
+SRC=$1
+NAME=$(dirname $SRC)
+ARCHS=$2
- cd $SRC
+cd $SRC
- for ARCH in $ARCHS; do
- # Output directory.
- PREFIX=runtimes/$ARCH/native
- # Build directory.
- BUILD=out/$ARCH
- # Cmake toolchain file.
- TOOLCHAIN=../cmake/$ARCH.cmake
+for ARCH in $ARCHS; do
+ # Output directory.
+ PREFIX=runtimes/$ARCH/native
+ # Build directory.
+ BUILD=out/$ARCH
+ # Cmake toolchain file.
+ TOOLCHAIN=../cmake/$ARCH.cmake
- # Create directories.
- mkdir -p $PREFIX $BUILD
- # Configure CMAKE.
- cmake -B $BUILD -S . \
- -DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_INSTALL_PREFIX=$PREFIX \
- -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN
-
- # Build and install.
- make -C $BUILD all
- make -C $BUILD install
- done
-
- mkdir bin
- nuget pack -OutputDirectory bin
- nuget push $(find bin/*.nupkg) -Source $QNUGET_LOCAL -NonInteractive
-}
+ # Create directories.
+ mkdir -p $PREFIX $BUILD
+ # Configure CMAKE.
+ cmake -B $BUILD -S . \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_INSTALL_PREFIX=$PREFIX \
+ -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN
+ # Build and install.
+ make -C $BUILD all
+ make -C $BUILD install
+done
diff --git a/tests/Quik.StbImage.Tests/LoadImage.cs b/tests/Quik.StbImage.Tests/LoadImage.cs
new file mode 100644
index 0000000..0fd97f9
--- /dev/null
+++ b/tests/Quik.StbImage.Tests/LoadImage.cs
@@ -0,0 +1,55 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Quik.Stb;
+
+namespace Quik.StbImage.Tests
+{
+ [TestClass]
+ public class LoadImage
+ {
+ [TestMethod("Set Global Options")]
+ public void SetGlobals()
+ {
+ Stbi.set_flip_vertically_on_load(1);
+ Stbi.set_unpremultiply_on_load(1);
+ }
+
+ private StbiStreamWrapper PrepareImage(string path, out stbi_io_callbacks cb)
+ {
+ Stream? str = GetType().Assembly.GetManifestResourceStream(path);
+ Assert.IsNotNull(str, $"Could not find test image resource {path}.");
+
+ StbiStreamWrapper wrapper = new StbiStreamWrapper(str);
+ wrapper.CreateCallbacks(out cb);
+ return wrapper;
+ }
+
+ [TestMethod("Load a PNG")]
+ public unsafe void LoadPng()
+ {
+ // TODO: Fill these up!
+ const int TEST_CHANNELS = 0;
+ const int TEST_WIDTH = 0;
+ const int TEST_HEIGHT = 0;
+
+ int x, y, numChannels;
+ using StbiStreamWrapper str = PrepareImage("Quik.StbImage.Tests.res.test.png", out stbi_io_callbacks cb);
+ byte *image = Stbi.load_from_callbacks(&cb, null, &x, &y, &numChannels, (int)StbiEnum.STBI_default);
+
+ if (image == null)
+ {
+ IntPtr reasonPtr = (IntPtr)Stbi.failure_reason();
+ string? reason = Marshal.PtrToStringUTF8((IntPtr)reasonPtr);
+ Assert.Fail("Could not load image: {0}.", reason ?? "None specified. (stbi_failure_reason() is null)");
+ }
+
+ Assert.AreEqual(TEST_CHANNELS, numChannels, "Channel count does not match.");
+ Assert.AreEqual(TEST_WIDTH, x, "Width does not match.");
+ Assert.AreEqual(TEST_HEIGHT, y, "Height does not match.");
+
+ Stbi.image_free(image);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Quik.StbImage.Tests/Quik.StbImage.Tests.csproj b/tests/Quik.StbImage.Tests/Quik.StbImage.Tests.csproj
new file mode 100644
index 0000000..238b7d3
--- /dev/null
+++ b/tests/Quik.StbImage.Tests/Quik.StbImage.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net6.0
+ disable
+ enable
+
+ false
+ true
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+