5 Commits

Author SHA1 Message Date
2f418866a4 Create a small demo application for StbImage. 2026-04-04 12:31:38 +03:00
776719648e Moved ReFuel.StbImage into its own subfolder. 2026-04-01 23:28:15 +03:00
b7983c96b2 [v2.1.0] Bump version number.
All checks were successful
Build / build (push) Successful in 1m58s
2024-11-18 20:55:49 +03:00
cb75b7c244 Made Callbacks a public field in StbiStreamWrapper.cs
All checks were successful
Build / build (push) Successful in 1m55s
2024-11-18 20:47:28 +03:00
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
11 changed files with 287 additions and 37 deletions

View File

@@ -0,0 +1,209 @@
/*
* ReFuel.StbImage.Viewer - A simple image viewer demo for ReFuel.StbImage.
* ------------------------------------------------------------------------
*
* Pass an image file as path (or drag and drop over the executable or window) to view
* of the file formats stb_image supports. Otherwise the default embedded image will be
* shown.
*
* The demo uses OpenGL2.1 for brevity sake - I did not feel like writing a shader program
* for this demo. It is very easy to port this demo to modern OpenGL if desired. Just
* replace the FFP calls with the equivalent programmable pipeline calls (glVertexAttribPointer,
* glUseShader and its friends).
*/
using System.Reflection;
using System.Runtime.InteropServices;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using ReFuel.Stb;
NativeWindow window = new NativeWindow(new NativeWindowSettings()
{
Profile = ContextProfile.Any,
APIVersion = new Version(2, 1),
Title = "ReFuel StbImage Viewer",
AutoLoadBindings = true,
Flags = ContextFlags.Default,
});
bool quit = false;
int texture = 0;
int imageWidth = 0, imageHeight = 0;
// This flag is important for OpenGL users, as texture coordinate systems
// are the opposite of what most image formats use. Y is up not down.
// Does not matter for DX, for example.
StbImage.FlipVerticallyOnLoad = true;
Vertex[] vertices = new Vertex[]
{
new Vertex(-1, -1, 0, 0),
new Vertex(1, -1, 1, 0),
new Vertex(1, 1, 1, 1),
new Vertex(-1, -1, 0, 0),
new Vertex(1, 1, 1, 1),
new Vertex(-1, 1, 0, 1),
};
LoadImage(args.Length > 0 ? args[0] : null);
GL.ClearColor(Color4.Black);
GL.Enable(EnableCap.Texture2D);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // conventional blending function.
GL.EnableClientState(ArrayCap.VertexArray);
GL.EnableClientState(ArrayCap.TextureCoordArray);
GL.EnableClientState(ArrayCap.ColorArray);
window.Closing += (_) => quit = true;
window.FramebufferResize += (_) => {
ResizeImage();
Paint();
};
window.FileDrop += (args) =>
LoadImage(
args.FileNames
.Select(x => new FileInfo(x))
.FirstOrDefault(x => x.Exists)?.FullName);
window.WindowState = WindowState.Normal;
while (!quit)
{
NativeWindow.ProcessWindowEvents(true);
Paint();
};
void Paint()
{
GL.Clear(ClearBufferMask.ColorBufferBit);
GL.Viewport(0, 0, window.FramebufferSize.X, window.FramebufferSize.Y);
GL.BindTexture(TextureTarget.Texture2D, texture);
unsafe
{
fixed (Vertex* pvert = vertices)
{
// We have to do it this way because the garbage collector may
// move the vertex data at any time. The OpenGL client will
// handle streaming the data at the glDrawArrays call, unlike
// the modern method.
GL.VertexPointer(2, VertexPointerType.Float, 32, (nint)(&pvert->X));
GL.TexCoordPointer(2, TexCoordPointerType.Float, 32, (nint)(&pvert->U));
GL.ColorPointer(4, ColorPointerType.Float, 32, (nint)(&pvert->Color));
GL.DrawArrays(PrimitiveType.Triangles, 0, vertices.Length);
}
}
window.Context.SwapBuffers();
}
void ResizeImage()
{
// Regenerates the vertex positions to maintain the aspect ratio and fill the window.
float windowAspect = (float)window.FramebufferSize.X / window.FramebufferSize.Y;
float imageAspect = (float)imageWidth / imageHeight;
float ratio = imageAspect / windowAspect;
float widthNDC, heightNDC;
if (ratio > 1)
{
widthNDC = 2.0f;
heightNDC = 2.0f / ratio;
}
else
{
heightNDC = 2.0f;
widthNDC = 2.0f * ratio;
}
vertices[0].X = -widthNDC / 2; vertices[0].Y = -heightNDC / 2;
vertices[1].X = widthNDC / 2; vertices[1].Y = -heightNDC / 2;
vertices[2].X = widthNDC / 2; vertices[2].Y = heightNDC / 2;
vertices[3].X = -widthNDC / 2; vertices[3].Y = -heightNDC / 2;
vertices[4].X = widthNDC / 2; vertices[4].Y = heightNDC / 2;
vertices[5].X = -widthNDC / 2; vertices[5].Y = heightNDC / 2;
}
void LoadImage(string? path)
{
// Load the given image, or the default if the path is null, or any other error happens.
StbImage? image = null;
if (path != null && File.Exists(path))
{
try
{
using Stream str = File.OpenRead(path);
StbImage.TryLoad(out image, str);
}
catch
{
// Ignore
}
}
if (image == null)
{
using Stream str = Assembly.GetExecutingAssembly().GetManifestResourceStream("default.png")!;
image = StbImage.Load(str);
}
// Boilerplate code for creating a new OpenGL texture.
GL.DeleteTexture(texture);
texture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, texture);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, image.Width, image.Height, 0, image.Format switch
{
StbiImageFormat.Grey => PixelFormat.Red,
StbiImageFormat.GreyAlpha => PixelFormat.Rg,
StbiImageFormat.Rgb => PixelFormat.Rgb,
StbiImageFormat.Rgba => PixelFormat.Rgba,
_ => throw new Exception()
}, image.IsFloat ? PixelType.Float : PixelType.UnsignedByte, image.ImagePointer);
// Generate mipmaps for better minification.
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
// Enable them.
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
// Set texture wrap mode to clamp to border to prevent bleeding on the image edges.
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder);
// For R or RA format images, we need to set the swizzle mask.
switch (image.Format)
{
case StbiImageFormat.Grey:
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.One);
break;
case StbiImageFormat.GreyAlpha:
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Green);
// Yes the last channel is green, we uploaded the texture with the Rg color format.
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.Red);
break;
}
imageWidth = image.Width;
imageHeight = image.Height;
ResizeImage();
image.Dispose();
}
// Vertex struct for convenience. Padded to 32 bytes for memory alignment.
[StructLayout(LayoutKind.Sequential, Size = 32)]
struct Vertex(float x, float y, float u, float v)
{
public float X = x, Y = y, U = u, V = v;
public Color4 Color = Color4.White;
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<!-- <ProjectReference Include="..\ReFuel.StbImage\ReFuel.StbImage.csproj" /> -->
<PackageReference Include="ReFuel.StbImage" Version="2.1.0"/>
<PackageReference Include="OpenTK" Version="4.9.4" />
<EmbeddedResource Include="../rf_stbimage.png" LogicalName="default.png"/>
</ItemGroup>
</Project>

View File

@@ -3,20 +3,46 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReFuel.StbImage", "ReFuel.StbImage.csproj", "{413ACBF4-3851-416F-B2A2-F7157EC306B2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReFuel.StbImage", "ReFuel.StbImage\ReFuel.StbImage.csproj", "{EB001CC8-6821-4579-BD5D-27C56A3C121E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReFuel.StbImage.Viewer", "ReFuel.StbImage.Viewer\ReFuel.StbImage.Viewer.csproj", "{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|x64.ActiveCfg = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|x64.Build.0 = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|x86.ActiveCfg = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Debug|x86.Build.0 = Debug|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|Any CPU.Build.0 = Release|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|x64.ActiveCfg = Release|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|x64.Build.0 = Release|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|x86.ActiveCfg = Release|Any CPU
{EB001CC8-6821-4579-BD5D-27C56A3C121E}.Release|x86.Build.0 = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|x64.ActiveCfg = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|x64.Build.0 = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|x86.ActiveCfg = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Debug|x86.Build.0 = Debug|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|Any CPU.Build.0 = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|x64.ActiveCfg = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|x64.Build.0 = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|x86.ActiveCfg = Release|Any CPU
{4CFB8D5B-8CE9-46EE-A34B-0D61693BDE50}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{413ACBF4-3851-416F-B2A2-F7157EC306B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{413ACBF4-3851-416F-B2A2-F7157EC306B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{413ACBF4-3851-416F-B2A2-F7157EC306B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{413ACBF4-3851-416F-B2A2-F7157EC306B2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal EndGlobal

View File

@@ -14,7 +14,7 @@
<!-- Nuget Properties. --> <!-- Nuget Properties. -->
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ReFuel.StbImage</PackageId> <PackageId>ReFuel.StbImage</PackageId>
<Version>2.0.2-rc.0</Version> <Version>2.1.0</Version>
<Authors>STBI Authors, H. Utku Maden</Authors> <Authors>STBI Authors, H. Utku Maden</Authors>
<Description> <Description>
A C# wrapper for the ubiquitous stb_image.h and stb_image_write.h library. A C# wrapper for the ubiquitous stb_image.h and stb_image_write.h library.
@@ -26,8 +26,10 @@
<RepositoryUrl>https://git.mixedup.dev/ReFuel/ReFuel.StbImage</RepositoryUrl> <RepositoryUrl>https://git.mixedup.dev/ReFuel/ReFuel.StbImage</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>stb; stb_image; stbi; image; load; save; read; write</PackageTags> <PackageTags>stb; stb_image; stbi; image; load; save; read; write</PackageTags>
<PackageReleaseNotes># 2.0.2 <PackageReleaseNotes># 2.1.0 (ABI BRAKING)
* Fixed calling convention related execution engine exception for the Windows platform. * Fixed calling convention of unmanaged function pointers. (Thanks NogginBops!)
* Modified StbiStreamWrapper in order to fixed backing delegates of function pointers from being prematurely collected
by release mode JIT and the GC. StbiStreamWrapper.Callbacks is now a readonly field. (ABI BREAKING)
# 2.0.1 # 2.0.1
* Enabled optimizations across the board for native and managed assemblies. * Enabled optimizations across the board for native and managed assemblies.
@@ -54,7 +56,7 @@
<Content Include="runtimes/linux-arm64/native/*.so"> <Content Include="runtimes/linux-arm64/native/*.so">
<PackagePath>runtimes/linux-arm64/native/</PackagePath> <PackagePath>runtimes/linux-arm64/native/</PackagePath>
<Pack>true</Pack> <Pack>true</Pack>
<CopyToOutputDirectory>PreserveNewest/</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="runtimes/linux-x64/native/*.so"> <Content Include="runtimes/linux-x64/native/*.so">
<PackagePath>runtimes/linux-x64/native/</PackagePath> <PackagePath>runtimes/linux-x64/native/</PackagePath>

View File

@@ -209,16 +209,18 @@ namespace ReFuel.Stb
{ {
int x, y, iFormat; int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true); StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
wrapper.CreateCallbacks(out stbi_io_callbacks cb);
stream.Position = 0; stream.Position = 0;
IntPtr imagePtr; IntPtr imagePtr;
if (asFloat) fixed (stbi_io_callbacks* cb = &wrapper.Callbacks)
{ {
imagePtr = (IntPtr)Stbi.loadf_from_callbacks(&cb, null, &x, &y, &iFormat, (int)format); if (asFloat)
} {
else imagePtr = (IntPtr)Stbi.loadf_from_callbacks(cb, null, &x, &y, &iFormat, (int)format);
{ }
imagePtr = (IntPtr)Stbi.load_from_callbacks(&cb, null, &x, &y, &iFormat, (int)format); else
{
imagePtr = (IntPtr)Stbi.load_from_callbacks(cb, null, &x, &y, &iFormat, (int)format);
}
} }
if (imagePtr != IntPtr.Zero) if (imagePtr != IntPtr.Zero)
@@ -317,10 +319,12 @@ namespace ReFuel.Stb
{ {
int x, y, iFormat; int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true); StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
wrapper.CreateCallbacks(out stbi_io_callbacks cb); int result;
stream.Position = 0; stream.Position = 0;
int result = Stbi.info_from_callbacks(&cb, null, &x, &y, &iFormat); fixed (stbi_io_callbacks* cb = &wrapper.Callbacks)
{
result = Stbi.info_from_callbacks(cb, null, &x, &y, &iFormat);
}
width = x; width = x;
height = y; height = y;

View File

@@ -32,11 +32,12 @@ namespace ReFuel.Stb
public unsafe delegate int StbiEofProc(void *userdata); public unsafe delegate int StbiEofProc(void *userdata);
/// <summary> /// <summary>
/// An easy to use stream wrapper for use with STBI image load functions. /// An easy-to-use stream wrapper for use with STBI image load functions.
/// </summary> /// </summary>
public unsafe class StbiStreamWrapper : IDisposable public unsafe class StbiStreamWrapper : IDisposable
{ {
private readonly stbi_io_callbacks _callbacks; public readonly stbi_io_callbacks Callbacks;
private readonly Stream _stream; private readonly Stream _stream;
private readonly bool _keepOpen; private readonly bool _keepOpen;
private bool _isDisposed; private bool _isDisposed;
@@ -45,8 +46,6 @@ namespace ReFuel.Stb
private StbiSkipProc _skipCb; private StbiSkipProc _skipCb;
private StbiEofProc _eofCb; private StbiEofProc _eofCb;
public ref readonly stbi_io_callbacks Callbacks => ref _callbacks;
public StbiStreamWrapper(Stream stream, bool keepOpen = false) public StbiStreamWrapper(Stream stream, bool keepOpen = false)
{ {
if (stream == null) throw new ArgumentNullException(nameof(stream)); if (stream == null) throw new ArgumentNullException(nameof(stream));
@@ -58,18 +57,10 @@ namespace ReFuel.Stb
_skipCb = SkipCb; _skipCb = SkipCb;
_eofCb = EofCb; _eofCb = EofCb;
_callbacks = default; Callbacks = default;
_callbacks.read = Marshal.GetFunctionPointerForDelegate<StbiReadProc>(_readCb); Callbacks.read = Marshal.GetFunctionPointerForDelegate<StbiReadProc>(_readCb);
_callbacks.skip = Marshal.GetFunctionPointerForDelegate<StbiSkipProc>(_skipCb); Callbacks.skip = Marshal.GetFunctionPointerForDelegate<StbiSkipProc>(_skipCb);
_callbacks.eof = Marshal.GetFunctionPointerForDelegate<StbiEofProc>(_eofCb); Callbacks.eof = Marshal.GetFunctionPointerForDelegate<StbiEofProc>(_eofCb);
}
public void CreateCallbacks(out stbi_io_callbacks cb)
{
cb = default;
cb.read = Marshal.GetFunctionPointerForDelegate<StbiReadProc>(_readCb);
cb.skip = Marshal.GetFunctionPointerForDelegate<StbiSkipProc>(_skipCb);
cb.eof = Marshal.GetFunctionPointerForDelegate<StbiEofProc>(_eofCb);
} }
private int ReadCb(void *userdata, byte* buffer, int count) private int ReadCb(void *userdata, byte* buffer, int count)

View File