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