Compare commits

...

2 Commits

Author SHA1 Message Date
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
7 changed files with 445 additions and 33 deletions

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>7.3</LangVersion>
<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>

@ -1,4 +1,6 @@
using ReFuel.Stb.Native;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
@ -50,6 +52,71 @@ namespace ReFuel.Stb
Dispose(false);
}
/// <summary>
/// Get a safe span to the image pointer.
/// </summary>
/// <typeparam name="T">The pixel type.</typeparam>
/// <returns>A span to the image data.</returns>
/// <exception cref="Exception">The image uses an unexpected image format.</exception>
public ReadOnlySpan<T> AsSpan<T>() where T : unmanaged
{
int sz = Format switch
{
StbiImageFormat.Grey => 1,
StbiImageFormat.GreyAlpha => 2,
StbiImageFormat.Rgb => 3,
StbiImageFormat.Rgba => 4,
_ => throw new Exception("unkown 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>
/// <remarks>
/// Incurs a conversion cost if the image format is not a byte format. Ignores alpha channel. Affected by non-thread safe global options.
/// </remarks>
public void WriteJpg(Stream dest, int quality = 90) => WriteJpg(AsSpan<byte>(), Width, Height, Format, dest, quality: quality, isFloat: IsFloat);
public void Dispose() => Dispose(true);
private void Dispose(bool disposing)
@ -68,7 +135,12 @@ namespace ReFuel.Stb
/// <summary>
/// Set to flip the y-axis of loaded images on load.
/// </summary>
public static bool FlipVerticallyOnLoad { set => Stbi.set_flip_vertically_on_load(1); }
public static bool FlipVerticallyOnLoad { set => Stbi.set_flip_vertically_on_load(value ? 1 : 0); }
/// <summary>
/// Set to flip the y-axis of saved images.
/// </summary>
public static bool FlipVerticallyOnSave { set => Stbi.flip_vertically_on_write(value ? 1 : 0); }
/// <summary>
/// Set to unpremultiply images on load.
@ -79,6 +151,50 @@ namespace ReFuel.Stb
/// </remarks>
public static bool UnpremultiplyOnLoad { set => Stbi.set_unpremultiply_on_load(1); }
/// <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 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 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 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>
@ -86,7 +202,7 @@ namespace ReFuel.Stb
/// <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)
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);
@ -94,7 +210,7 @@ namespace ReFuel.Stb
stream.Position = 0;
IntPtr imagePtr;
if (isFloat)
if (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_callbacks(&cb, null, &x, &y, &iFormat, (int)format);
}
@ -105,7 +221,7 @@ namespace ReFuel.Stb
if (imagePtr != IntPtr.Zero)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, asFloat);
return true;
}
else
@ -122,25 +238,24 @@ namespace ReFuel.Stb
/// <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
public static bool TryLoad([NotNullWhen(true)] out StbImage? image, ReadOnlySpan<byte> span, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
IntPtr imagePtr = IntPtr.Zero;
int x, y, iFormat;
fixed (byte *ptr = MemoryMarshal.AsBytes(span))
{
if (isFloat)
if (asFloat)
{
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.loadf_from_memory(ptr, span.Length, &x, &y, &iFormat, (int)format);
}
else
{
imagePtr = (IntPtr)Stbi.load_from_memory(ptr, span.Length * sizeof(T), &x, &y, &iFormat, (int)format);
imagePtr = (IntPtr)Stbi.load_from_memory(ptr, span.Length, &x, &y, &iFormat, (int)format);
}
if (imagePtr != IntPtr.Zero)
{
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, isFloat);
image = new StbImage(imagePtr, x, y, (StbiImageFormat)iFormat, asFloat);
return true;
}
else
@ -157,14 +272,14 @@ namespace ReFuel.Stb
/// <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)
public static StbImage Load(Stream stream, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
if (TryLoad(out StbImage image, stream, format, isFloat))
if (TryLoad(out StbImage? image, stream, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
@ -174,15 +289,14 @@ namespace ReFuel.Stb
/// <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
public static StbImage Load(ReadOnlySpan<byte> span, StbiImageFormat format = StbiImageFormat.Default, bool asFloat = false)
{
if (TryLoad(out StbImage image, span, format, isFloat))
if (TryLoad(out StbImage? image, span, format, asFloat))
{
return image;
}
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason());
string reason = Marshal.PtrToStringUTF8((IntPtr)Stbi.failure_reason())!;
throw new Exception($"Failed to load image: {reason}");
}
@ -230,5 +344,222 @@ namespace ReFuel.Stb
return result != 0;
}
}
private static int Components(StbiImageFormat format) => format switch
{
StbiImageFormat.Grey => 1,
StbiImageFormat.GreyAlpha => 2,
StbiImageFormat.Rgb => 3,
StbiImageFormat.Rgba => 4,
_ => throw new ArgumentException("Expected a fully qualified format.")
};
private static byte[] ConvertFloatToByte<T>(ReadOnlySpan<T> source, int width, int height, int components)
where T : unmanaged
{
byte[] conversion = new byte[width * height * components];
ReadOnlySpan<float> dataAsFloat = MemoryMarshal.Cast<T, float>(source);
for (int i = 0; i<conversion.Length; i++)
{
conversion[i] = (byte) Math.Clamp(MathF.Round(dataAsFloat[i]* 255), 0, 255);
}
return conversion;
}
private static float[] ConvertByteToFloat<T>(ReadOnlySpan<T> source, int width, int height, int components)
where T : unmanaged
{
float[] conversion = new float[width * height * components];
ReadOnlySpan<byte> dataAsByte = MemoryMarshal.Cast<T, byte>(source);
for (int i = 0; i < conversion.Length; i++)
{
conversion[i] = Math.Clamp(dataAsByte[i]/255f, 0f, 1f);
}
return conversion;
}
/// <summary>
/// Write any image to a PNG file.
/// </summary>
/// <typeparam name="T">Any packed byte or float array structure.</typeparam>
/// <param name="data">Span of pixel data.</param>
/// <param name="width">Width of the pixel data in pixels.</param>
/// <param name="height">Height of the pixel data in pixels.</param>
/// <param name="format">Color format of the pixel data. Must not be <see cref="StbiImageFormat.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.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.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.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.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 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);
}
}
}

@ -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);
}
}
}

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

@ -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,

@ -1,23 +1,47 @@
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>
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>
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>
public unsafe delegate int StbiEofProc(void *userdata);
/// <summary>
/// An easy to use stream wrapper for use with STBI image load functions.
/// </summary>
public unsafe class StbiStreamWrapper : IDisposable
{
private Stream _stream;
private bool _keepOpen;
private readonly stbi_io_callbacks _callbacks;
private readonly Stream _stream;
private readonly bool _keepOpen;
private bool _isDisposed;
private StbiReadProc _readCb;
private StbiSkipProc _skipCb;
private StbiEofProc _eofCb;
public ref readonly stbi_io_callbacks Callbacks => ref _callbacks;
public StbiStreamWrapper(Stream stream, bool keepOpen = false)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
@ -28,6 +52,11 @@ namespace ReFuel.Stb
_readCb = ReadCb;
_skipCb = SkipCb;
_eofCb = EofCb;
_callbacks = default;
_callbacks.read = Marshal.GetFunctionPointerForDelegate<StbiReadProc>(_readCb);
_callbacks.skip = Marshal.GetFunctionPointerForDelegate<StbiSkipProc>(_skipCb);
_callbacks.eof = Marshal.GetFunctionPointerForDelegate<StbiEofProc>(_eofCb);
}
public void CreateCallbacks(out stbi_io_callbacks cb)
@ -65,4 +94,32 @@ namespace ReFuel.Stb
_isDisposed = true;
}
}
/// <summary>
/// An easy to use stream wrapper for STBI image write functions.
/// </summary>
/// <remarks>Keep struct alive for the duration of the write operation.</remarks>
public struct StbiWriteStreamWrapper
{
private readonly Stream _stream;
private readonly StbiWriteProc _cb;
public IntPtr Callback => Marshal.GetFunctionPointerForDelegate(_cb);
public StbiWriteStreamWrapper(Stream stream)
{
_stream = stream;
unsafe
{
_cb = WriteCb;
}
}
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;
}
}

@ -1,15 +1,21 @@
using System;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
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>
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* _forced_png_filter_ptr;
private static readonly int* _force_png_filter_ptr;
public static int write_tga_with_rle
{
@ -25,8 +31,8 @@ namespace ReFuel.Stb
public static int write_force_png_filter
{
get => *_forced_png_filter_ptr;
set => *_forced_png_filter_ptr = value;
get => *_force_png_filter_ptr;
set => *_force_png_filter_ptr = value;
}
[DllImport("stbi", CallingConvention = CallingConvention.Cdecl, EntryPoint = "stbi_write_png_to_func")]