using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
namespace ReFuel.Stb
{
///
/// A class that encompasses all features of stb_image.h in a safe way.
///
public unsafe class StbImage : IDisposable
{
private bool isDisposed = false;
///
/// Pointer to the image.
///
public IntPtr ImagePointer { get; }
///
/// Width of the image.
///
public int Width { get; }
///
/// Height of the image.
///
///
public int Height { get; }
///
/// Internal image format.
///
public StbiImageFormat Format { get; }
///
/// True if the image is a floating point image.
///
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);
}
///
/// Get a safe span to the image pointer.
///
/// The pixel type.
/// A span to the image data.
/// The image uses an unexpected image format.
public ReadOnlySpan AsSpan() 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*)ImagePointer, Width * Height * sz / sizeof(T));
}
///
/// Write image to a PNG file.
///
/// The destination stream.
///
/// Incurs a conversion cost if the image format is not a byte format. Affected by non-thread safe global options.
///
public void WritePng(Stream dest) => WritePng(AsSpan(), Width, Height, Format, dest, isFloat: IsFloat);
///
/// Write image to a BMP file.
///
/// The destination stream.
///
/// Incurs a conversion cost if the image format is not a byte format. Affected by non-thread safe global options.
///
public void WriteBmp(Stream dest) => WriteBmp(AsSpan(), Width, Height, Format, dest, isFloat: IsFloat);
///
/// Write image to a TGA file.
///
/// The destination stream.
///
/// Incurs a conversion cost if the image format is not a byte format. Ignores alpha channel. Affected by non-thread safe global options.
///
public void WriteTga(Stream dest) => WriteTga(AsSpan(), Width, Height, Format, dest, isFloat: IsFloat);
///
/// Write image to a PNG file.
///
/// The destination stream.
///
/// Incurs a conversion cost if the image format is not a float format. Affected by non-thread safe global options.
///
public void WriteHdr(Stream dest) => WriteHdr(AsSpan(), Width, Height, Format, dest, isFloat: IsFloat);
///
/// Write image to a PNG file.
///
/// The destination stream.
///
/// Incurs a conversion cost if the image format is not a byte format. Ignores alpha channel. Affected by non-thread safe global options.
///
public void WriteJpg(Stream dest, int quality = 90) => WriteJpg(AsSpan(), 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;
}
///
/// Set to flip the y-axis of loaded images on load.
///
public static bool FlipVerticallyOnLoad { set => Stbi.set_flip_vertically_on_load(value ? 1 : 0); }
///
/// Set to flip the y-axis of saved images.
///
public static bool FlipVerticallyOnSave { set => Stbi.flip_vertically_on_write(value ? 1 : 0); }
///
/// Set to unpremultiply images on load.
///
///
/// According to the stb_image documentation, only iPhone PNG images
/// can come with premultiplied alpha.
///
public static bool UnpremultiplyOnLoad { set => Stbi.set_unpremultiply_on_load(1); }
///
/// Force a filter on PNG filter when saving.
///
///
/// -1 for auto, 0 through 5 to pick a filter. Higher is more. Not thread safe.
///
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;
}
}
///
/// Change the PNG compression level on save.
///
///
/// Higher is more. Defaults to 8. Not thread safe.
///
public int WritePngCompressionLevel
{
get => Stbi.write_png_compression_level;
set => Stbi.write_png_compression_level = value;
}
///
/// Enable run length encoding on TGA images on save.
///
///
/// Not thread safe.
///
public bool WriteTgaEnableRLE
{
get => Stbi.write_tga_with_rle != 0;
set => Stbi.write_tga_with_rle = value ? 1 : 0;
}
///
/// Try loading an image, without raising exceptions.
///
/// The resulting image.
/// Source stream.
/// The desired image format.
/// True on success.
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);
wrapper.CreateCallbacks(out stbi_io_callbacks cb);
stream.Position = 0;
IntPtr imagePtr;
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;
}
}
///
/// Try loading an image, without raising exceptions.
///
/// The resulting image.
/// Source memory span.
/// The desired image format.
/// True on success.
public static bool TryLoad([NotNullWhen(true)] out StbImage? image, ReadOnlySpan 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;
}
}
}
///
/// Load an image.
///
/// The stream to load from.
/// The desired image format.
/// The image object.
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}");
}
///
/// Load an image.
///
/// The span of memory to load from.
/// The desired image format.
/// The image object.
public static StbImage Load(ReadOnlySpan 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}");
}
///
/// Peek image info from a stream.
///
/// The stream to peek into.
/// Width of the image.
/// Height of the image.
/// The image format.
/// True if the stream contained an image.
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;
}
///
/// Peek image info from a span.
///
/// The span to peek into.
/// Width of the image.
/// Height of the image.
/// The image format.
/// True if the stream contained an image.
public static bool PeekInfo(ReadOnlySpan 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(ReadOnlySpan source, int width, int height, int components)
where T : unmanaged
{
byte[] conversion = new byte[width * height * components];
ReadOnlySpan dataAsFloat = MemoryMarshal.Cast(source);
for (int i = 0; i(ReadOnlySpan source, int width, int height, int components)
where T : unmanaged
{
float[] conversion = new float[width * height * components];
ReadOnlySpan dataAsByte = MemoryMarshal.Cast(source);
for (int i = 0; i < conversion.Length; i++)
{
conversion[i] = Math.Clamp(dataAsByte[i]/255f, 0f, 1f);
}
return conversion;
}
///
/// Write any image to a PNG file.
///
/// Any packed byte or float array structure.
/// Span of pixel data.
/// Width of the pixel data in pixels.
/// Height of the pixel data in pixels.
/// Color format of the pixel data. Must not be .
/// The destination stream.
/// True if the pixel format in data is a floating point format.
///
/// This will incur a conversion cost if the pixel format is not a byte format. Affected by global non-thread safe options.
///
public static void WritePng(ReadOnlySpan data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan 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);
}
///
/// Write any image to a BMP file.
///
/// Any packed byte or float array structure.
/// Span of pixel data.
/// Width of the pixel data in pixels.
/// Height of the pixel data in pixels.
/// Color format of the pixel data. Must not be .
/// The destination stream.
/// True if the pixel format in data is a floating point format.
///
/// 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.
///
public static void WriteBmp(ReadOnlySpan data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan 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);
}
///
/// Write any image to a TGA file.
///
/// Any packed byte or float array structure.
/// Span of pixel data.
/// Width of the pixel data in pixels.
/// Height of the pixel data in pixels.
/// Color format of the pixel data. Must not be .
/// The destination stream.
/// True if the pixel format in data is a floating point format.
///
/// This will incur a conversion cost if the pixel format is not a byte format. Affected by global non-thread safe options.
///
public static void WriteTga(ReadOnlySpan data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan 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);
}
///
/// Write any image to a PNG file.
///
/// Any packed byte or float array structure.
/// Span of pixel data.
/// Width of the pixel data in pixels.
/// Height of the pixel data in pixels.
/// Color format of the pixel data. Must not be .
/// The destination stream.
/// True if the pixel format in data is a floating point format.
///
/// This will incur a conversion cost if the pixel format is not a float format. Affected by global non-thread safe options.
///
public static void WriteHdr(ReadOnlySpan data, int width, int height, StbiImageFormat format, Stream destination, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan source;
float[]? conversion;
if (!isFloat)
{
conversion = ConvertByteToFloat(data, width, height, components);
source = conversion;
}
else
{
source = MemoryMarshal.Cast(data);
}
StbiWriteStreamWrapper wrapper = new StbiWriteStreamWrapper(destination);
fixed (float* ptr = source)
Stbi.write_hdr_to_func(wrapper, null, width, height, components, ptr);
}
///
/// Write any image to a PNG file.
///
/// Any packed byte or float array structure.
/// Span of pixel data.
/// Width of the pixel data in pixels.
/// Height of the pixel data in pixels.
/// Color format of the pixel data. Must not be .
/// The destination stream.
/// True if the pixel format in data is a floating point format.
///
/// 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.
///
public static void WriteJpg(ReadOnlySpan data, int width, int height, StbiImageFormat format, Stream destination, int quality = 90, bool isFloat = false)
where T : unmanaged
{
int components = Components(format);
ReadOnlySpan 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);
}
}
}