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 + + + + + + + + + + + + + +