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