diff --git a/sources/engine/Stride/Graphics/StandardImageHelper.Desktop.cs b/sources/engine/Stride/Graphics/StandardImageHelper.Desktop.cs
index f204210143..338a30cf65 100644
--- a/sources/engine/Stride/Graphics/StandardImageHelper.Desktop.cs
+++ b/sources/engine/Stride/Graphics/StandardImageHelper.Desktop.cs
@@ -1,115 +1,284 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
#if STRIDE_PLATFORM_DESKTOP
+
using System;
using System.IO;
using System.Runtime.InteropServices;
+
using FreeImageAPI;
+
using Stride.Core;
-using RotateFlipType = FreeImageAPI.RotateFlipType;
-namespace Stride.Graphics
+namespace Stride.Graphics;
+
+internal partial class StandardImageHelper
{
+ static StandardImageHelper()
+ {
+ NativeLibraryHelper.PreloadLibrary("freeimage", typeof(StandardImageHelper));
+ }
+
///
- /// This class is responsible to provide image loader for png, gif, bmp.
+ /// Loads an image from a block of unmanaged memory.
///
- partial class StandardImageHelper
+ ///
+ /// A pointer to the beginning of the unmanaged memory block containing the image data.
+ ///
+ ///
+ /// The size, in bytes, of the memory block pointed to by .
+ ///
+ ///
+ /// A value indicating whether to make a copy of the image data (),
+ /// or to use the provided memory directly ().
+ /// If , the method may free the memory after loading.
+ ///
+ ///
+ /// An optional associated with the memory block.
+ /// If provided, the handle will be freed after loading the image.
+ ///
+ /// An object containing the loaded image data.
+ ///
+ /// The image is loaded with a pixel format of
+ /// and is vertically flipped to match expected orientation.
+ ///
+ public static unsafe Image LoadFromMemory(IntPtr pSource, int size, bool makeACopy, GCHandle? handle)
{
- static StandardImageHelper()
- {
- NativeLibraryHelper.PreloadLibrary("freeimage", typeof(StandardImageHelper));
- }
+ using var memoryStream = new UnmanagedMemoryStream((byte*) pSource, size, capacity: size, access: FileAccess.Read);
+ using var bitmap = FreeImageBitmap.FromStream(memoryStream);
- public static unsafe Image LoadFromMemory(IntPtr pSource, int size, bool makeACopy, GCHandle? handle)
- {
- using var memoryStream = new UnmanagedMemoryStream((byte*)pSource, size, capacity: size, access: FileAccess.Read);
- using var bitmap = FreeImageBitmap.FromStream(memoryStream);
-
- bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
- bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
-
- var image = Image.New2D(bitmap.Width, bitmap.Height, 1, PixelFormat.B8G8R8A8_UNorm, 1, bitmap.Line);
-
- try
- {
- // TODO: Test if still necessary
- // Directly load image as RGBA instead of BGRA, because OpenGL ES devices don't support it out of the box (extension).
- MemoryUtilities.CopyWithAlignmentFallback((void*)image.PixelBuffer[0].DataPointer, (void*)bitmap.Bits, (uint)image.PixelBuffer[0].BufferStride);
- }
- finally
- {
- if (handle != null)
- handle.Value.Free();
- else if (!makeACopy)
- MemoryUtilities.Free(pSource);
- }
-
- return image;
- }
+ bitmap.RotateFlip(FreeImageAPI.RotateFlipType.RotateNoneFlipY);
+ bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
- public static void SaveGifFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
- {
- SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_GIF);
- }
+ var image = Image.New2D(bitmap.Width, bitmap.Height,
+ mipMapCount: 1, arraySize: 1, rowStride: bitmap.Line,
+ format: PixelFormat.B8G8R8A8_UNorm);
- public static void SaveTiffFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ try
{
- SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_TIFF);
+ // TODO: Test if still necessary
+ // Directly load image as RGBA instead of BGRA, because OpenGL ES devices don't support it out of the box (extension).
+ MemoryUtilities.CopyWithAlignmentFallback(destination: (void*) image.PixelBuffer[0].DataPointer,
+ source: (void*) bitmap.Bits,
+ byteCount: (uint) image.PixelBuffer[0].BufferStride);
}
-
- public static void SaveBmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ finally
{
- SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_BMP);
+ if (handle is not null)
+ handle.Value.Free();
+ else if (!makeACopy)
+ MemoryUtilities.Free(pSource);
}
- public static void SaveJpgFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ return image;
+ }
+
+
+ ///
+ /// Saves a GIF image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the GIF image will be written. The stream must be writable.
+ ///
+ public static void SaveGifFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ using var bitmap = new FreeImageBitmap(description.Width, description.Height);
+ PrepareImageForSaving(bitmap, pixelBuffers, description);
+ bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_GIF);
+ }
+
+ ///
+ /// Saves a TIFF image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the TIFF image will be written. The stream must be writable.
+ ///
+ public static void SaveTiffFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ using var bitmap = new FreeImageBitmap(description.Width, description.Height);
+ PrepareImageForSaving(bitmap, pixelBuffers, description);
+ bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_TIFF);
+ }
+
+ ///
+ /// Saves a BMP image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the BMP image will be written. The stream must be writable.
+ ///
+ public static void SaveBmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ using var bitmap = new FreeImageBitmap(description.Width, description.Height);
+ PrepareImageForSaving(bitmap, pixelBuffers, description);
+ bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_BMP);
+ }
+
+ ///
+ /// Saves a JPEG image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the JPEG image will be written. The stream must be writable.
+ ///
+ ///
+ /// The image is saved with a default quality of 90 and 4:2:0 chroma subsampling.
+ ///
+ public static void SaveJpgFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ using var bitmap = new FreeImageBitmap(description.Width, description.Height);
+ PrepareImageForSaving(bitmap, pixelBuffers, description);
+
+ // Set JPEG quality to 90 and 4:2:0 subsampling by default
+ var flags = (FREE_IMAGE_SAVE_FLAGS) 90
+ | FREE_IMAGE_SAVE_FLAGS.JPEG_SUBSAMPLING_420;
+
+ bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_JPEG, flags);
+ }
+
+ ///
+ /// Saves a PNG image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the PNG image will be written. The stream must be writable.
+ ///
+ public static void SavePngFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ using var bitmap = new FreeImageBitmap(description.Width, description.Height);
+ PrepareImageForSaving(bitmap, pixelBuffers, description);
+ bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_PNG);
+ }
+
+ ///
+ /// Saves a WMP (Windows Media Photo) image to the specified stream using pixel data from memory.
+ ///
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ ///
+ /// The number of pixel buffers to use when saving the image.
+ /// Must be greater than zero and less than or equal to the length of .
+ ///
+ ///
+ /// An structure that specifies the properties of the image,
+ /// such as width, height, and pixel format.
+ ///
+ ///
+ /// The stream to which the WMP image will be written. The stream must be writable.
+ ///
+ /// The method is not implemented.
+ public static void SaveWmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ {
+ throw new NotImplementedException();
+ }
+
+
+ ///
+ /// Prepares the specified bitmap for saving by converting its color depth to 32 bits per pixel,
+ /// copying pixel data from the provided buffers according to the image format,
+ /// and flipping the image vertically.
+ ///
+ /// The bitmap to be prepared for saving.
+ ///
+ /// An array of pixel buffers containing the image data to copy into the bitmap.
+ ///
+ /// The description of the image.
+ ///
+ /// The pixel format specified in is not supported.
+ /// Supported formats are , ,
+ /// , ,
+ /// , and .
+ ///
+ private static unsafe void PrepareImageForSaving(FreeImageBitmap bitmap, PixelBuffer[] pixelBuffers, ImageDescription description)
+ {
+ // Ensure 32 bits per pixel
+ bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
+
+ // Copy the image data according to the format
+ var format = description.Format;
+ if (format is PixelFormat.R8G8B8A8_UNorm or PixelFormat.R8G8B8A8_UNorm_SRgb)
{
- SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_JPEG);
+ CopyMemoryBGRA(dest: bitmap.Bits, src: pixelBuffers[0].DataPointer,
+ sizeInBytesToCopy: pixelBuffers[0].BufferStride);
}
-
- public static void SavePngFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ else if (format is PixelFormat.B8G8R8A8_UNorm or PixelFormat.B8G8R8A8_UNorm_SRgb)
{
- SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_PNG);
+ MemoryUtilities.CopyWithAlignmentFallback(destination: (void*) bitmap.Bits,
+ source: (void*) pixelBuffers[0].DataPointer,
+ byteCount: (uint) pixelBuffers[0].BufferStride);
}
-
- public static void SaveWmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
+ else if (format is PixelFormat.R8_UNorm or PixelFormat.A8_UNorm)
{
- throw new NotImplementedException();
+ // TODO: Ideally we will want to support grayscale images, but SpriteBatch can only render RGBA for now,
+ // so convert the grayscale image as RGBA and save it
+ CopyMemoryRRR1(dest: bitmap.Bits, src: pixelBuffers[0].DataPointer,
+ sizeInBytesToCopy: pixelBuffers[0].BufferStride);
}
-
- private static unsafe void SaveFromMemory(PixelBuffer[] pixelBuffers, ImageDescription description, Stream imageStream, FREE_IMAGE_FORMAT imageFormat)
+ else
{
- using var bitmap = new FreeImageBitmap(description.Width, description.Height);
- bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
-
- // Copy memory
- var format = description.Format;
- if (format is PixelFormat.R8G8B8A8_UNorm or PixelFormat.R8G8B8A8_UNorm_SRgb)
- {
- CopyMemoryBGRA(bitmap.Bits, pixelBuffers[0].DataPointer, pixelBuffers[0].BufferStride);
- }
- else if (format is PixelFormat.B8G8R8A8_UNorm or PixelFormat.B8G8R8A8_UNorm_SRgb)
- {
- MemoryUtilities.CopyWithAlignmentFallback((void*)bitmap.Bits, (void*)pixelBuffers[0].DataPointer, (uint)pixelBuffers[0].BufferStride);
- }
- else if (format is PixelFormat.R8_UNorm or PixelFormat.A8_UNorm)
- {
- // TODO Ideally we will want to support grayscale images, but the SpriteBatch can only render RGBA for now
- // so convert the grayscale image as an RGBA and save it
- CopyMemoryRRR1(bitmap.Bits, pixelBuffers[0].DataPointer, pixelBuffers[0].BufferStride);
- }
- else
- {
- throw new ArgumentException(
- message:
- $"The pixel format {format} is not supported. Supported formats are {PixelFormat.B8G8R8A8_UNorm}, {PixelFormat.B8G8R8A8_UNorm_SRgb}, {PixelFormat.R8G8B8A8_UNorm}, {PixelFormat.R8G8B8A8_UNorm_SRgb}, {PixelFormat.R8_UNorm}, {PixelFormat.A8_UNorm}",
- paramName: nameof(description));
- }
-
- // Save
- bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
- bitmap.Save(imageStream, imageFormat);
+ throw new ArgumentException(
+ message:
+ $"The pixel format {format} is not supported. Supported formats are {PixelFormat.B8G8R8A8_UNorm}, {PixelFormat.B8G8R8A8_UNorm_SRgb}, {PixelFormat.R8G8B8A8_UNorm}, {PixelFormat.R8G8B8A8_UNorm_SRgb}, {PixelFormat.R8_UNorm}, and {PixelFormat.A8_UNorm}",
+ paramName: nameof(description));
}
+
+ // Flip the image vertically
+ bitmap.RotateFlip(FreeImageAPI.RotateFlipType.RotateNoneFlipY);
}
}
+
#endif
diff --git a/sources/engine/Stride/Graphics/StandardImageHelper.cs b/sources/engine/Stride/Graphics/StandardImageHelper.cs
index 7bf088d92f..679fcc9a3d 100644
--- a/sources/engine/Stride/Graphics/StandardImageHelper.cs
+++ b/sources/engine/Stride/Graphics/StandardImageHelper.cs
@@ -1,48 +1,74 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
using System;
using System.Buffers.Binary;
using System.Numerics;
-namespace Stride.Graphics
+namespace Stride.Graphics;
+
+///
+/// Provides implementations to load and save s of different formats
+/// (e.g., PNG, GIF, BMP, JPEG, etc.)
+///
+internal partial class StandardImageHelper
{
///
- /// This class is responsible to provide image loader for png, gif, bmp.
+ /// Copies a block of memory from a source buffer to a destination buffer,
+ /// converting each 32-bit pixel from RGBA to BGRA format.
///
- partial class StandardImageHelper
+ /// A pointer to the destination buffer that will receive the converted BGRA pixel data.
+ /// A pointer to the source buffer containing the RGBA pixel data to copy and convert.
+ ///
+ /// The number of bytes to copy and convert. Must be a multiple of 4, as each pixel is represented by 4 bytes.
+ ///
+ /// is not a multiple of 4.
+ ///
+ /// The conversion swaps the red and blue channels for each pixel, effectively transforming the format
+ /// from RGBA to BGRA.
+ ///
+ private static unsafe void CopyMemoryBGRA(IntPtr dest, IntPtr src, int sizeInBytesToCopy)
{
- private static unsafe void CopyMemoryBGRA(IntPtr dest, IntPtr src, int sizeInBytesToCopy)
- {
- if ((sizeInBytesToCopy & 3) != 0)
- throw new ArgumentException("Should be a multiple of 4.", "sizeInBytesToCopy");
+ if ((sizeInBytesToCopy & 3) != 0)
+ throw new ArgumentException("Should be a multiple of 4.", nameof(sizeInBytesToCopy));
- var bufferSize = sizeInBytesToCopy / 4;
- var srcPtr = (uint*)src;
- var destPtr = (uint*)dest;
- for (int i = 0; i < bufferSize; ++i)
- {
- var value = *srcPtr++;
- // value: 0xAARRGGBB or in reverse 0xAABBGGRR
- value = BinaryPrimitives.ReverseEndianness(value);
- // value: 0xBBGGRRAA or in reverse 0xRRGGBBAA
- value = BitOperations.RotateRight(value, 8);
- // value: 0xAABBGGRR or in reverse 0xAARRGGBB
- *destPtr++ = value;
- }
+ var bufferSize = sizeInBytesToCopy / 4;
+ var srcPtr = (uint*) src;
+ var destPtr = (uint*) dest;
+ for (int i = 0; i < bufferSize; ++i)
+ {
+ var value = *srcPtr++;
+ // value: 0xAARRGGBB or in reverse 0xAABBGGRR
+ value = BinaryPrimitives.ReverseEndianness(value);
+ // value: 0xBBGGRRAA or in reverse 0xRRGGBBAA
+ value = BitOperations.RotateRight(value, 8);
+ // value: 0xAABBGGRR or in reverse 0xAARRGGBB
+ *destPtr++ = value;
}
+ }
- private static unsafe void CopyMemoryRRR1(IntPtr dest, IntPtr src, int sizeInBytesToCopy)
+ ///
+ /// Copies a block of memory from a source buffer to a destination buffer,
+ /// converting each source byte representing a red channel value into a 32-bit RGBA pixel
+ /// with full opacity and equal red, green, and blue channels.
+ ///
+ /// A pointer to the destination buffer where the converted RGBA pixel data will be written.
+ /// A pointer to the source buffer containing the red channel byte values to copy and convert.
+ ///
+ /// The number of bytes to copy and convert from the source buffer.
+ /// Each byte is treated as a single red channel value and converted to one 32-bit RGBA pixel.
+ ///
+ private static unsafe void CopyMemoryRRR1(IntPtr dest, IntPtr src, int sizeInBytesToCopy)
+ {
+ var bufferSize = sizeInBytesToCopy;
+ var srcPtr = (byte*)src;
+ var destPtr = (uint*)dest;
+ for (int i = 0; i < bufferSize; ++i)
{
- var bufferSize = sizeInBytesToCopy;
- var srcPtr = (byte*)src;
- var destPtr = (uint*)dest;
- for (int i = 0; i < bufferSize; ++i)
- {
- uint value = *srcPtr++;
- // R => RGBA
- value = 0xFF000000u | (value * 0x010101u);
- *destPtr++ = value;
- }
+ uint value = *srcPtr++;
+ // R => RGBA
+ value = 0xFF000000u | (value * 0x010101u);
+ *destPtr++ = value;
}
}
}