Fixed bug 3352 - Adding alpha mask support to SDL_SaveBMP_RW

Simon Hug

The current SDL_SaveBMP_RW function that saves surfaces to a BMP uses an old bitmap header which doesn't officially support alpha channels. Applications just ignore the byte where the alpha is stored. This can easily be extended by using a newer header version and setting the alpha mask.

The attached patch has these changes:

- Extending the description of the function in the SDL_surface.h header with the supported formats.
- Refining when surfaces get stored to a 32-bit BMP. (Must have bit depth of 8 or higher and must have an alpha mask or colorkey.)
- Fixing a small bug that saves 24-bit BGR surfaces with a colorkey in a 24-bit BMP.
- Adding code that switches to the bitmap header version 4 if the surface has an alpha mask or colorkey. (I chose version 4 because Microsoft didn't lose its documentation behind a file cabinet like they did with version 3.)
- Adding a hint that can disable the use of the version 4 header. This is for people that need the legacy header or like the old behavior better. (I'm not sure about the hint name, though. May need changing if there are any rules to that.)
diff --git a/include/SDL_hints.h b/include/SDL_hints.h
index f27a582..fdb4d2b 100644
--- a/include/SDL_hints.h
+++ b/include/SDL_hints.h
@@ -670,6 +670,25 @@
 #define SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4	"SDL_WINDOWS_NO_CLOSE_ON_ALT_F4"
 
 /**
+ *  \brief Prevent SDL from using version 4 of the bitmap header when saving BMPs.
+ *
+ * The bitmap header version 4 is required for proper alpha channel support and
+ * SDL will use it when required. Should this not be desired, this hint can
+ * force the use of the 40 byte header version which is supported everywhere.
+ *
+ * The variable can be set to the following values:
+ *   "0"       - Surfaces with a colorkey or an alpha channel are saved to a
+ *               32-bit BMP file with an alpha mask. SDL will use the bitmap
+ *               header version 4 and set the alpha mask accordingly.
+ *   "1"       - Surfaces with a colorkey or an alpha channel are saved to a
+ *               32-bit BMP file without an alpha mask. The alpha channel data
+ *               will be in the file, but applications are going to ignore it.
+ *
+ * The default value is "0".
+ */
+#define SDL_HINT_BMP_SAVE_LEGACY_FORMAT "SDL_BMP_SAVE_LEGACY_FORMAT"
+
+/**
  *  \brief  An enumeration of hint priorities
  */
 typedef enum
diff --git a/include/SDL_surface.h b/include/SDL_surface.h
index e63ca89..0fc65da 100644
--- a/include/SDL_surface.h
+++ b/include/SDL_surface.h
@@ -184,6 +184,12 @@
 /**
  *  Save a surface to a seekable SDL data stream (memory or file).
  *
+ *  Surfaces with a 24-bit, 32-bit and paletted 8-bit format get saved in the
+ *  BMP directly. Other RGB formats with 8-bit or higher get converted to a
+ *  24-bit surface or, if they have an alpha mask or a colorkey, to a 32-bit
+ *  surface before they are saved. YUV and paletted 1-bit and 4-bit formats are
+ *  not supported.
+ *
  *  If \c freedst is non-zero, the stream will be closed after being written.
  *
  *  \return 0 if successful or -1 if there was an error.
diff --git a/src/video/SDL_bmp.c b/src/video/SDL_bmp.c
index f80f936..e71c6cc 100644
--- a/src/video/SDL_bmp.c
+++ b/src/video/SDL_bmp.c
@@ -32,6 +32,7 @@
    This code currently supports Win32 DIBs in uncompressed 8 and 24 bpp.
 */
 
+#include "SDL_hints.h"
 #include "SDL_video.h"
 #include "SDL_assert.h"
 #include "SDL_endian.h"
@@ -47,6 +48,11 @@
 #define BI_BITFIELDS    3
 #endif
 
+/* Logical color space values for BMP files */
+#ifndef LCS_WINDOWS_COLOR_SPACE
+/* 0x57696E20 == "Win " */
+#define LCS_WINDOWS_COLOR_SPACE    0x57696E20
+#endif
 
 static void CorrectAlphaChannel(SDL_Surface *surface)
 {
@@ -457,6 +463,8 @@
     int i, pad;
     SDL_Surface *surface;
     Uint8 *bits;
+    SDL_bool save32bit = SDL_FALSE;
+    SDL_bool saveLegacyBMP = SDL_FALSE;
 
     /* The Win32 BMP file header (14 bytes) */
     char magic[2] = { 'B', 'M' };
@@ -478,14 +486,24 @@
     Uint32 biClrUsed;
     Uint32 biClrImportant;
 
+    /* The additional header members from the Win32 BITMAPV4HEADER struct (108 bytes in total) */
+    Uint32 bV4RedMask = 0;
+    Uint32 bV4GreenMask = 0;
+    Uint32 bV4BlueMask = 0;
+    Uint32 bV4AlphaMask = 0;
+    Uint32 bV4CSType = 0;
+    Sint32 bV4Endpoints[3 * 3] = {0};
+    Uint32 bV4GammaRed = 0;
+    Uint32 bV4GammaGreen = 0;
+    Uint32 bV4GammaBlue = 0;
+
     /* Make sure we have somewhere to save */
     surface = NULL;
     if (dst) {
-        SDL_bool save32bit = SDL_FALSE;
 #ifdef SAVE_32BIT_BMP
         /* We can save alpha information in a 32-bit BMP */
-        if (saveme->map->info.flags & SDL_COPY_COLORKEY ||
-            saveme->format->Amask) {
+        if (saveme->format->BitsPerPixel >= 8 && (saveme->format->Amask ||
+            saveme->map->info.flags & SDL_COPY_COLORKEY)) {
             save32bit = SDL_TRUE;
         }
 #endif /* SAVE_32BIT_BMP */
@@ -497,7 +515,7 @@
                 SDL_SetError("%d bpp BMP files not supported",
                              saveme->format->BitsPerPixel);
             }
-        } else if ((saveme->format->BitsPerPixel == 24) &&
+        } else if ((saveme->format->BitsPerPixel == 24) && !save32bit &&
 #if SDL_BYTEORDER == SDL_LIL_ENDIAN
                    (saveme->format->Rmask == 0x00FF0000) &&
                    (saveme->format->Gmask == 0x0000FF00) &&
@@ -537,6 +555,13 @@
         return -1;
     }
 
+    if (save32bit) {
+        const char *hint = SDL_GetHint(SDL_HINT_BMP_SAVE_LEGACY_FORMAT);
+        if (hint != NULL && (hint[0] == '1' && hint[1] == 0)) {
+            saveLegacyBMP = SDL_TRUE;
+        }
+    }
+
     if (surface && (SDL_LockSurface(surface) == 0)) {
         const int bw = surface->w * surface->format->BytesPerPixel;
 
@@ -572,6 +597,21 @@
         }
         biClrImportant = 0;
 
+        /* Set the BMP info values for the version 4 header */
+        if (save32bit && !saveLegacyBMP) {
+            biSize = 108;
+            biCompression = BI_BITFIELDS;
+            /* The BMP format is always little endian, these masks stay the same */
+            bV4RedMask   = 0x00ff0000;
+            bV4GreenMask = 0x0000ff00;
+            bV4BlueMask  = 0x000000ff;
+            bV4AlphaMask = 0xff000000;
+            bV4CSType = LCS_WINDOWS_COLOR_SPACE;
+            bV4GammaRed = 0;
+            bV4GammaGreen = 0;
+            bV4GammaBlue = 0;
+        }
+
         /* Write the BMP info values */
         SDL_WriteLE32(dst, biSize);
         SDL_WriteLE32(dst, biWidth);
@@ -585,6 +625,21 @@
         SDL_WriteLE32(dst, biClrUsed);
         SDL_WriteLE32(dst, biClrImportant);
 
+        /* Write the BMP info values for the version 4 header */
+        if (save32bit && !saveLegacyBMP) {
+            SDL_WriteLE32(dst, bV4RedMask);
+            SDL_WriteLE32(dst, bV4GreenMask);
+            SDL_WriteLE32(dst, bV4BlueMask);
+            SDL_WriteLE32(dst, bV4AlphaMask);
+            SDL_WriteLE32(dst, bV4CSType);
+            for (i = 0; i < 3 * 3; i++) {
+                SDL_WriteLE32(dst, bV4Endpoints[i]);
+            }
+            SDL_WriteLE32(dst, bV4GammaRed);
+            SDL_WriteLE32(dst, bV4GammaGreen);
+            SDL_WriteLE32(dst, bV4GammaBlue);
+        }
+
         /* Write the palette (in BGR color order) */
         if (surface->format->palette) {
             SDL_Color *colors;