Skip to content

Commit 966fdb7

Browse files
authored
fix: Set default quality flags when saving in JPEG format (#3034)
* Fix JPEG saving and refactor StandardImageHelper * Save JPEGs with quality 90 and chroma subsampling 4:2:0 by default. * Reorganize the class to better allow format-specific tweaks. * Improve documentation for StandardImageHelper
1 parent 6496f7e commit 966fdb7

File tree

2 files changed

+309
-114
lines changed

2 files changed

+309
-114
lines changed
Lines changed: 252 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,284 @@
11
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
22
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
34
#if STRIDE_PLATFORM_DESKTOP
5+
46
using System;
57
using System.IO;
68
using System.Runtime.InteropServices;
9+
710
using FreeImageAPI;
11+
812
using Stride.Core;
9-
using RotateFlipType = FreeImageAPI.RotateFlipType;
1013

11-
namespace Stride.Graphics
14+
namespace Stride.Graphics;
15+
16+
internal partial class StandardImageHelper
1217
{
18+
static StandardImageHelper()
19+
{
20+
NativeLibraryHelper.PreloadLibrary("freeimage", typeof(StandardImageHelper));
21+
}
22+
1323
/// <summary>
14-
/// This class is responsible to provide image loader for png, gif, bmp.
24+
/// Loads an image from a block of unmanaged memory.
1525
/// </summary>
16-
partial class StandardImageHelper
26+
/// <param name="pSource">
27+
/// A pointer to the beginning of the unmanaged memory block containing the image data.
28+
/// </param>
29+
/// <param name="size">
30+
/// The size, in bytes, of the memory block pointed to by <paramref name="pSource"/>.
31+
/// </param>
32+
/// <param name="makeACopy">
33+
/// A value indicating whether to make a copy of the image data (<see langword="true"/>),
34+
/// or to use the provided memory directly (<see langword="false"/>).
35+
/// If <see langword="false"/>, the method may free the memory after loading.
36+
/// </param>
37+
/// <param name="handle">
38+
/// An optional <see cref="GCHandle"/> associated with the memory block.
39+
/// If provided, the handle will be freed after loading the image.
40+
/// </param>
41+
/// <returns>An <see cref="Image"/> object containing the loaded image data.</returns>
42+
/// <remarks>
43+
/// The image is loaded with a pixel format of <see cref="PixelFormat.B8G8R8A8_UNorm"/>
44+
/// and is vertically flipped to match expected orientation.
45+
/// </remarks>
46+
public static unsafe Image LoadFromMemory(IntPtr pSource, int size, bool makeACopy, GCHandle? handle)
1747
{
18-
static StandardImageHelper()
19-
{
20-
NativeLibraryHelper.PreloadLibrary("freeimage", typeof(StandardImageHelper));
21-
}
48+
using var memoryStream = new UnmanagedMemoryStream((byte*) pSource, size, capacity: size, access: FileAccess.Read);
49+
using var bitmap = FreeImageBitmap.FromStream(memoryStream);
2250

23-
public static unsafe Image LoadFromMemory(IntPtr pSource, int size, bool makeACopy, GCHandle? handle)
24-
{
25-
using var memoryStream = new UnmanagedMemoryStream((byte*)pSource, size, capacity: size, access: FileAccess.Read);
26-
using var bitmap = FreeImageBitmap.FromStream(memoryStream);
27-
28-
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
29-
bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
30-
31-
var image = Image.New2D(bitmap.Width, bitmap.Height, 1, PixelFormat.B8G8R8A8_UNorm, 1, bitmap.Line);
32-
33-
try
34-
{
35-
// TODO: Test if still necessary
36-
// Directly load image as RGBA instead of BGRA, because OpenGL ES devices don't support it out of the box (extension).
37-
MemoryUtilities.CopyWithAlignmentFallback((void*)image.PixelBuffer[0].DataPointer, (void*)bitmap.Bits, (uint)image.PixelBuffer[0].BufferStride);
38-
}
39-
finally
40-
{
41-
if (handle != null)
42-
handle.Value.Free();
43-
else if (!makeACopy)
44-
MemoryUtilities.Free(pSource);
45-
}
46-
47-
return image;
48-
}
51+
bitmap.RotateFlip(FreeImageAPI.RotateFlipType.RotateNoneFlipY);
52+
bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
4953

50-
public static void SaveGifFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
51-
{
52-
SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_GIF);
53-
}
54+
var image = Image.New2D(bitmap.Width, bitmap.Height,
55+
mipMapCount: 1, arraySize: 1, rowStride: bitmap.Line,
56+
format: PixelFormat.B8G8R8A8_UNorm);
5457

55-
public static void SaveTiffFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
58+
try
5659
{
57-
SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_TIFF);
60+
// TODO: Test if still necessary
61+
// Directly load image as RGBA instead of BGRA, because OpenGL ES devices don't support it out of the box (extension).
62+
MemoryUtilities.CopyWithAlignmentFallback(destination: (void*) image.PixelBuffer[0].DataPointer,
63+
source: (void*) bitmap.Bits,
64+
byteCount: (uint) image.PixelBuffer[0].BufferStride);
5865
}
59-
60-
public static void SaveBmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
66+
finally
6167
{
62-
SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_BMP);
68+
if (handle is not null)
69+
handle.Value.Free();
70+
else if (!makeACopy)
71+
MemoryUtilities.Free(pSource);
6372
}
6473

65-
public static void SaveJpgFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
74+
return image;
75+
}
76+
77+
78+
/// <summary>
79+
/// Saves a GIF image to the specified stream using pixel data from memory.
80+
/// </summary>
81+
/// <param name="pixelBuffers">
82+
/// An array of pixel buffers containing the image data to copy into the bitmap.
83+
/// </param>
84+
/// <param name="count">
85+
/// The number of pixel buffers to use when saving the image.
86+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
87+
/// </param>
88+
/// <param name="description">
89+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
90+
/// such as width, height, and pixel format.
91+
/// </param>
92+
/// <param name="imageStream">
93+
/// The stream to which the GIF image will be written. The stream must be writable.
94+
/// </param>
95+
public static void SaveGifFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
96+
{
97+
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
98+
PrepareImageForSaving(bitmap, pixelBuffers, description);
99+
bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_GIF);
100+
}
101+
102+
/// <summary>
103+
/// Saves a TIFF image to the specified stream using pixel data from memory.
104+
/// </summary>
105+
/// <param name="pixelBuffers">
106+
/// An array of pixel buffers containing the image data to copy into the bitmap.
107+
/// </param>
108+
/// <param name="count">
109+
/// The number of pixel buffers to use when saving the image.
110+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
111+
/// </param>
112+
/// <param name="description">
113+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
114+
/// such as width, height, and pixel format.
115+
/// </param>
116+
/// <param name="imageStream">
117+
/// The stream to which the TIFF image will be written. The stream must be writable.
118+
/// </param>
119+
public static void SaveTiffFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
120+
{
121+
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
122+
PrepareImageForSaving(bitmap, pixelBuffers, description);
123+
bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_TIFF);
124+
}
125+
126+
/// <summary>
127+
/// Saves a BMP image to the specified stream using pixel data from memory.
128+
/// </summary>
129+
/// <param name="pixelBuffers">
130+
/// An array of pixel buffers containing the image data to copy into the bitmap.
131+
/// </param>
132+
/// <param name="count">
133+
/// The number of pixel buffers to use when saving the image.
134+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
135+
/// </param>
136+
/// <param name="description">
137+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
138+
/// such as width, height, and pixel format.
139+
/// </param>
140+
/// <param name="imageStream">
141+
/// The stream to which the BMP image will be written. The stream must be writable.
142+
/// </param>
143+
public static void SaveBmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
144+
{
145+
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
146+
PrepareImageForSaving(bitmap, pixelBuffers, description);
147+
bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_BMP);
148+
}
149+
150+
/// <summary>
151+
/// Saves a JPEG image to the specified stream using pixel data from memory.
152+
/// </summary>
153+
/// <param name="pixelBuffers">
154+
/// An array of pixel buffers containing the image data to copy into the bitmap.
155+
/// </param>
156+
/// <param name="count">
157+
/// The number of pixel buffers to use when saving the image.
158+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
159+
/// </param>
160+
/// <param name="description">
161+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
162+
/// such as width, height, and pixel format.
163+
/// </param>
164+
/// <param name="imageStream">
165+
/// The stream to which the JPEG image will be written. The stream must be writable.
166+
/// </param>
167+
/// <remarks>
168+
/// The image is saved with a default quality of 90 and 4:2:0 chroma subsampling.
169+
/// </remarks>
170+
public static void SaveJpgFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
171+
{
172+
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
173+
PrepareImageForSaving(bitmap, pixelBuffers, description);
174+
175+
// Set JPEG quality to 90 and 4:2:0 subsampling by default
176+
var flags = (FREE_IMAGE_SAVE_FLAGS) 90
177+
| FREE_IMAGE_SAVE_FLAGS.JPEG_SUBSAMPLING_420;
178+
179+
bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_JPEG, flags);
180+
}
181+
182+
/// <summary>
183+
/// Saves a PNG image to the specified stream using pixel data from memory.
184+
/// </summary>
185+
/// <param name="pixelBuffers">
186+
/// An array of pixel buffers containing the image data to copy into the bitmap.
187+
/// </param>
188+
/// <param name="count">
189+
/// The number of pixel buffers to use when saving the image.
190+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
191+
/// </param>
192+
/// <param name="description">
193+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
194+
/// such as width, height, and pixel format.
195+
/// </param>
196+
/// <param name="imageStream">
197+
/// The stream to which the PNG image will be written. The stream must be writable.
198+
/// </param>
199+
public static void SavePngFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
200+
{
201+
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
202+
PrepareImageForSaving(bitmap, pixelBuffers, description);
203+
bitmap.Save(imageStream, FREE_IMAGE_FORMAT.FIF_PNG);
204+
}
205+
206+
/// <summary>
207+
/// Saves a WMP (Windows Media Photo) image to the specified stream using pixel data from memory.
208+
/// </summary>
209+
/// <param name="pixelBuffers">
210+
/// An array of pixel buffers containing the image data to copy into the bitmap.
211+
/// </param>
212+
/// <param name="count">
213+
/// The number of pixel buffers to use when saving the image.
214+
/// Must be greater than zero and less than or equal to the length of <paramref name="pixelBuffers"/>.
215+
/// </param>
216+
/// <param name="description">
217+
/// An <see cref="ImageDescription"/> structure that specifies the properties of the image,
218+
/// such as width, height, and pixel format.
219+
/// </param>
220+
/// <param name="imageStream">
221+
/// The stream to which the WMP image will be written. The stream must be writable.
222+
/// </param>
223+
/// <exception cref="NotImplementedException">The method is not implemented.</exception>
224+
public static void SaveWmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
225+
{
226+
throw new NotImplementedException();
227+
}
228+
229+
230+
/// <summary>
231+
/// Prepares the specified bitmap for saving by converting its color depth to 32 bits per pixel,
232+
/// copying pixel data from the provided buffers according to the image format,
233+
/// and flipping the image vertically.
234+
/// </summary>
235+
/// <param name="bitmap">The bitmap to be prepared for saving.</param>
236+
/// <param name="pixelBuffers">
237+
/// An array of pixel buffers containing the image data to copy into the bitmap.
238+
/// </param>
239+
/// <param name="description">The description of the image.</param>
240+
/// <exception cref="ArgumentException">
241+
/// The pixel format specified in <paramref name="description"/> is not supported.
242+
/// Supported formats are <see cref="PixelFormat.B8G8R8A8_UNorm"/>, <see cref="PixelFormat.B8G8R8A8_UNorm_SRgb"/>,
243+
/// <see cref="PixelFormat.R8G8B8A8_UNorm"/>, <see cref="PixelFormat.R8G8B8A8_UNorm_SRgb"/>,
244+
/// <see cref="PixelFormat.R8_UNorm"/>, and <see cref="PixelFormat.A8_UNorm"/>.
245+
/// </exception>
246+
private static unsafe void PrepareImageForSaving(FreeImageBitmap bitmap, PixelBuffer[] pixelBuffers, ImageDescription description)
247+
{
248+
// Ensure 32 bits per pixel
249+
bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
250+
251+
// Copy the image data according to the format
252+
var format = description.Format;
253+
if (format is PixelFormat.R8G8B8A8_UNorm or PixelFormat.R8G8B8A8_UNorm_SRgb)
66254
{
67-
SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_JPEG);
255+
CopyMemoryBGRA(dest: bitmap.Bits, src: pixelBuffers[0].DataPointer,
256+
sizeInBytesToCopy: pixelBuffers[0].BufferStride);
68257
}
69-
70-
public static void SavePngFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
258+
else if (format is PixelFormat.B8G8R8A8_UNorm or PixelFormat.B8G8R8A8_UNorm_SRgb)
71259
{
72-
SaveFromMemory(pixelBuffers, description, imageStream, FREE_IMAGE_FORMAT.FIF_PNG);
260+
MemoryUtilities.CopyWithAlignmentFallback(destination: (void*) bitmap.Bits,
261+
source: (void*) pixelBuffers[0].DataPointer,
262+
byteCount: (uint) pixelBuffers[0].BufferStride);
73263
}
74-
75-
public static void SaveWmpFromMemory(PixelBuffer[] pixelBuffers, int count, ImageDescription description, Stream imageStream)
264+
else if (format is PixelFormat.R8_UNorm or PixelFormat.A8_UNorm)
76265
{
77-
throw new NotImplementedException();
266+
// TODO: Ideally we will want to support grayscale images, but SpriteBatch can only render RGBA for now,
267+
// so convert the grayscale image as RGBA and save it
268+
CopyMemoryRRR1(dest: bitmap.Bits, src: pixelBuffers[0].DataPointer,
269+
sizeInBytesToCopy: pixelBuffers[0].BufferStride);
78270
}
79-
80-
private static unsafe void SaveFromMemory(PixelBuffer[] pixelBuffers, ImageDescription description, Stream imageStream, FREE_IMAGE_FORMAT imageFormat)
271+
else
81272
{
82-
using var bitmap = new FreeImageBitmap(description.Width, description.Height);
83-
bitmap.ConvertColorDepth(FREE_IMAGE_COLOR_DEPTH.FICD_32_BPP);
84-
85-
// Copy memory
86-
var format = description.Format;
87-
if (format is PixelFormat.R8G8B8A8_UNorm or PixelFormat.R8G8B8A8_UNorm_SRgb)
88-
{
89-
CopyMemoryBGRA(bitmap.Bits, pixelBuffers[0].DataPointer, pixelBuffers[0].BufferStride);
90-
}
91-
else if (format is PixelFormat.B8G8R8A8_UNorm or PixelFormat.B8G8R8A8_UNorm_SRgb)
92-
{
93-
MemoryUtilities.CopyWithAlignmentFallback((void*)bitmap.Bits, (void*)pixelBuffers[0].DataPointer, (uint)pixelBuffers[0].BufferStride);
94-
}
95-
else if (format is PixelFormat.R8_UNorm or PixelFormat.A8_UNorm)
96-
{
97-
// TODO Ideally we will want to support grayscale images, but the SpriteBatch can only render RGBA for now
98-
// so convert the grayscale image as an RGBA and save it
99-
CopyMemoryRRR1(bitmap.Bits, pixelBuffers[0].DataPointer, pixelBuffers[0].BufferStride);
100-
}
101-
else
102-
{
103-
throw new ArgumentException(
104-
message:
105-
$"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}",
106-
paramName: nameof(description));
107-
}
108-
109-
// Save
110-
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
111-
bitmap.Save(imageStream, imageFormat);
273+
throw new ArgumentException(
274+
message:
275+
$"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}",
276+
paramName: nameof(description));
112277
}
278+
279+
// Flip the image vertically
280+
bitmap.RotateFlip(FreeImageAPI.RotateFlipType.RotateNoneFlipY);
113281
}
114282
}
283+
115284
#endif

0 commit comments

Comments
 (0)