20 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
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
19 changed files with 1069 additions and 340 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

@@ -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
VisualStudioVersion = 17.0.31903.59
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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

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.1</Version>
<Version>2.1.0</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.1.0 (ABI BRAKING)
* 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
* 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>
@@ -40,7 +56,7 @@
<Content Include="runtimes/linux-arm64/native/*.so">
<PackagePath>runtimes/linux-arm64/native/</PackagePath>
<Pack>true</Pack>
<CopyToOutputDirectory>PreserveNewest/</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="runtimes/linux-x64/native/*.so">
<PackagePath>runtimes/linux-x64/native/</PackagePath>

575
ReFuel.StbImage/StbImage.cs Normal file
View File

@@ -0,0 +1,575 @@
using ReFuel.Stb.Native;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
{
/// <summary>
/// A class that encompasses all features of stb_image.h in a safe way.
/// </summary>
public unsafe class StbImage : IDisposable
{
private bool isDisposed = false;
/// <summary>
/// Pointer to the image.
/// </summary>
public IntPtr ImagePointer { get; }
/// <summary>
/// Width of the image.
/// </summary>
public int Width { get; }
/// <summary>
/// Height of the image.
/// </summary>
/// <value></value>
public int Height { get; }
/// <summary>
/// Internal image format.
/// </summary>
public StbiImageFormat Format { get; }
/// <summary>
/// True if the image is a floating point image.
/// </summary>
public bool IsFloat { get; }
private StbImage(IntPtr image, int x, int y, StbiImageFormat format, bool isFloat)
{
ImagePointer = image;
Width = x;
Height = y;
Format = format;
IsFloat = isFloat;
}
~StbImage()
{
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)
{
if (isDisposed) return;
if (disposing)
{
GC.SuppressFinalize(this);
}
Stbi.image_free(ImagePointer.ToPointer());
isDisposed = true;
}
/// <summary>
/// Set to flip the y-axis of loaded images on load.
/// </summary>
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.
/// </summary>
/// <remarks>
/// 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(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.
/// </summary>
/// <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([NotNullWhen(true)] out StbImage? image, Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
stream.Position = 0;
IntPtr imagePtr;
fixed (stbi_io_callbacks* cb = &wrapper.Callbacks)
{
if (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_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)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, asFloat);
return true;
}
else
{
image = null;
return false;
}
}
/// <summary>
/// Try loading an image, without raising exceptions.
/// </summary>
/// <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([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 (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length, &x, &y, &iFormat, (int)format);
}
else
{
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, asFloat);
return true;
}
else
{
image = null;
return false;
}
}
}
/// <summary>
/// Load an image.
/// </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 asFloat = false)
{
if (TryLoad(out StbImage? image, stream, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
/// <summary>
/// Load an image.
/// </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(ReadOnlySpan<byte> span, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
if (TryLoad(out StbImage? image, span, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
/// <summary>
/// Peek image info from a stream.
/// </summary>
/// <param name="stream">The stream to peek into.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="format">The image format.</param>
/// <returns>True if the stream contained an image.</returns>
public static bool PeekInfo(Stream stream, out int width, out int height, out StbiImageFormat format)
{
int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
int result;
stream.Position = 0;
fixed (stbi_io_callbacks* cb = &wrapper.Callbacks)
{
result = Stbi.info_from_callbacks(cb, null, &x, &y, &iFormat);
}
width = x;
height = y;
format = (StbiImageFormat)iFormat;
return result != 0;
}
/// <summary>
/// Peek image info from a span.
/// </summary>
/// <param name="span">The span to peek into.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="format">The image format.</param>
/// <returns>True if the stream contained an image.</returns>
public static bool PeekInfo<T>(ReadOnlySpan<T> span, out int width, out int height, out StbiImageFormat format) where T : unmanaged
{
fixed (byte* ptr = MemoryMarshal.AsBytes(span))
{
int x, y, iFormat;
int result = Stbi.info_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat);
width = x;
height = y;
format = (StbiImageFormat)iFormat;
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

@@ -0,0 +1,122 @@
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
{
public 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 StbiStreamWrapper(Stream stream, bool keepOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
_stream = stream;
_keepOpen = keepOpen;
_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);
}
private int ReadCb(void *userdata, byte* buffer, int count)
{
Span<byte> bytes = new Span<byte>(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;
}
}
/// <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;
}
}

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,234 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
{
/// <summary>
/// A class that encompasses all features of stb_image.h in a safe way.
/// </summary>
public unsafe class StbImage : IDisposable
{
private bool isDisposed = false;
/// <summary>
/// Pointer to the image.
/// </summary>
public IntPtr ImagePointer { get; }
/// <summary>
/// Width of the image.
/// </summary>
public int Width { get; }
/// <summary>
/// Height of the image.
/// </summary>
/// <value></value>
public int Height { get; }
/// <summary>
/// Internal image format.
/// </summary>
public StbiImageFormat Format { get; }
/// <summary>
/// True if the image is a floating point image.
/// </summary>
public bool IsFloat { get; }
private StbImage(IntPtr image, int x, int y, StbiImageFormat format, bool isFloat)
{
ImagePointer = image;
Width = x;
Height = y;
Format = format;
IsFloat = isFloat;
}
~StbImage()
{
Dispose(false);
}
public void Dispose() => Dispose(true);
private void Dispose(bool disposing)
{
if (isDisposed) return;
if (disposing)
{
GC.SuppressFinalize(this);
}
Stbi.image_free(ImagePointer.ToPointer());
isDisposed = true;
}
/// <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); }
/// <summary>
/// Set to unpremultiply images on load.
/// </summary>
/// <remarks>
/// 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); }
/// <summary>
/// Try loading an image, without raising exceptions.
/// </summary>
/// <param name="image">The resulting image.</param>
/// <param name="stream">Source stream.</param>
/// <param name="format">The desired image format.</param>
/// <returns>True on success.</returns>
public static bool TryLoad(out StbImage image, Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
{
int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
wrapper.CreateCallbacks(out stbi_io_callbacks cb);
stream.Position = 0;
IntPtr imagePtr;
if (isFloat)
{
imagePtr = (IntPtr)Stbi.loadf_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)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
return true;
}
else
{
image = null;
return false;
}
}
/// <summary>
/// Try loading an image, without raising exceptions.
/// </summary>
/// <param name="image">The resulting image.</param>
/// <param name="span">Source memory span.</param>
/// <param name="format">The desired image format.</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
{
IntPtr imagePtr = IntPtr.Zero;
int x, y, iFormat;
fixed (byte *ptr = MemoryMarshal.AsBytes(span))
{
if (isFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
}
else
{
imagePtr = (IntPtr)Stbi.load_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
}
if (imagePtr != IntPtr.Zero)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
return true;
}
else
{
image = null;
return false;
}
}
}
/// <summary>
/// Load an image.
/// </summary>
/// <param name="stream">The stream to load from.</param>
/// <param name="format">The desired image format.</param>
/// <returns>The image object.</returns>
public static StbImage Load(Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
{
if (TryLoad(out StbImage image, stream, format, isFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
throw new Exception($"Failed to load image: {reason}");
}
/// <summary>
/// Load an image.
/// </summary>
/// <param name="span">The span of memory to load from.</param>
/// <param name="format">The desired image format.</param>
/// <returns>The image object.</returns>
public static StbImage Load<T>(ReadOnlySpan<T> span, StbiImageFormat format = StbiImageFormat.Default, bool isFloat = false)
where T : unmanaged
{
if (TryLoad(out StbImage image, span, format, isFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
throw new Exception($"Failed to load image: {reason}");
}
/// <summary>
/// Peek image info from a stream.
/// </summary>
/// <param name="stream">The stream to peek into.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="format">The image format.</param>
/// <returns>True if the stream contained an image.</returns>
public static bool PeekInfo(Stream stream, out int width, out int height, out StbiImageFormat format)
{
int x, y, iFormat;
StbiStreamWrapper wrapper = new StbiStreamWrapper(stream, true);
wrapper.CreateCallbacks(out stbi_io_callbacks cb);
stream.Position = 0;
int result = Stbi.info_from_callbacks(&cb, null, &x, &y, &iFormat);
width = x;
height = y;
format = (StbiImageFormat)iFormat;
return result != 0;
}
/// <summary>
/// Peek image info from a span.
/// </summary>
/// <param name="span">The span to peek into.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="format">The image format.</param>
/// <returns>True if the stream contained an image.</returns>
public static bool PeekInfo<T>(ReadOnlySpan<T> span, out int width, out int height, out StbiImageFormat format) where T : unmanaged
{
fixed (byte* ptr = MemoryMarshal.AsBytes(span))
{
int x, y, iFormat;
int result = Stbi.info_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat);
width = x;
height = y;
format = (StbiImageFormat)iFormat;
return result != 0;
}
}
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
{
public unsafe delegate int StbiReadProc(void *userdata, byte* buffer, int count);
public unsafe delegate void StbiSkipProc(void *userdata, int count);
public unsafe delegate int StbiEofProc(void *userdata);
public unsafe class StbiStreamWrapper : IDisposable
{
private Stream _stream;
private bool _keepOpen;
private bool _isDisposed;
private StbiReadProc _readCb;
private StbiSkipProc _skipCb;
private StbiEofProc _eofCb;
public StbiStreamWrapper(Stream stream, bool keepOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
_stream = stream;
_keepOpen = keepOpen;
_readCb = ReadCb;
_skipCb = SkipCb;
_eofCb = 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)
{
Span<byte> bytes = new Span<byte>(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;
}
}
}

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

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